# Listings — Eto API

> Read and edit single listings: detail, images, inventory, reviews.

Base URL `https://eto.tools/api/v1/`. Every request carries the `X-Eto-API-Key` header. Part of the [Eto API reference](https://eto.tools/dev/docs/).

## Endpoints in this category

- [GET /api/v1/listings/{listing_id}](https://eto.tools/dev/docs/listings/listing-detail.md): Get listing details
- [DELETE /api/v1/listings/{listing_id}](https://eto.tools/dev/docs/listings/listing-delete.md): Delete a listing
- [GET /api/v1/listings/{listing_id}/images](https://eto.tools/dev/docs/listings/listing-images.md): Get listing images
- [GET /api/v1/listings/{listing_id}/reviews](https://eto.tools/dev/docs/listings/listing-reviews.md): Get listing reviews
- [GET /api/v1/listings/{listing_id}/inventory](https://eto.tools/dev/docs/listings/listing-inventory-get.md): Get listing inventory
- [PUT /api/v1/listings/{listing_id}/inventory](https://eto.tools/dev/docs/listings/listing-inventory-update.md): Update listing inventory
- [GET/PUT /api/v1/shops/{shop_id}/listings/{listing_id}/inventory](https://eto.tools/dev/docs/listings/shop-listing-inventory.md): Get or update listing inventory (shop-scoped)
- [GET /api/v1/listings/{listing_id}/videos](https://eto.tools/dev/docs/listings/listing-videos.md): Get listing videos
- [GET /api/v1/listings/{listing_id}/personalization](https://eto.tools/dev/docs/listings/listing-personalization-get.md): Get listing personalization
- [POST /api/v1/listings/{listing_id}/personalization](https://eto.tools/dev/docs/listings/listing-personalization-post.md): Set listing personalization
- [DELETE /api/v1/listings/{listing_id}/personalization](https://eto.tools/dev/docs/listings/listing-personalization-delete.md): Delete listing personalization
- [POST/DELETE /api/v1/shops/{shop_id}/listings/{listing_id}/personalization](https://eto.tools/dev/docs/listings/shop-listing-personalization.md): Set or delete personalization (shop-scoped)
- [GET /api/v1/listings/batch](https://eto.tools/dev/docs/listings/listings-batch.md): Batch get listings
- [GET /api/v1/listings/batch/inventory](https://eto.tools/dev/docs/listings/listings-batch-inventory.md): Batch get listing inventory
- [GET /api/v1/listings/{listing_id}/products/{product_id}/offerings/{offering_id}](https://eto.tools/dev/docs/listings/listing-offering.md): Get a listing offering
- [GET /api/v1/listings/{listing_id}/inventory/products/{product_id}](https://eto.tools/dev/docs/listings/listing-product.md): Get a product variant

## GET /api/v1/listings/{listing_id}

Get listing details

- Operation ID: `listing_detail`
- Slug: `listing-detail`
- Category: Listings
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-detail
- Markdown: https://eto.tools/dev/docs/listings/listing-detail.md

Retrieve detailed information about a specific listing including title, description, price, tags, materials, and more.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |
| `includes` | query | `string` | no | Comma-separated associations to include: Images, Shop, User, Translations, Inventory, Videos |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890?includes=Images,Shop"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890?includes=Images,Shop"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890?includes=Images,Shop";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.


## DELETE /api/v1/listings/{listing_id}

Delete a listing

- Operation ID: `listing_delete`
- Slug: `listing-delete`
- Category: Listings
- Authentication: API key plus a connected store. Eto uses that store’s Etsy token, so the shop_id must belong to a store connected to your Eto account or the call answers 403 STORE_NOT_CONNECTED.
- HTML: https://eto.tools/dev/docs/#ep-listing-delete
- Markdown: https://eto.tools/dev/docs/listings/listing-delete.md

Permanently delete a listing you own. This action cannot be undone. Eto finds the connected store that owns the listing; a listing outside your stores answers 403 LISTING_NOT_IN_CONNECTED_STORE.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID to delete |

### Request

```bash
curl -X DELETE -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.delete(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890";

const response = await fetch(url, {
  method: "DELETE",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## GET /api/v1/listings/{listing_id}/images

Get listing images

- Operation ID: `listing_images`
- Slug: `listing-images`
- Category: Listings
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-images
- Markdown: https://eto.tools/dev/docs/listings/listing-images.md

Retrieve all images for a specific listing.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/images`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/images"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/images"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/images";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/images` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.


## GET /api/v1/listings/{listing_id}/reviews

Get listing reviews

- Operation ID: `listing_reviews`
- Slug: `listing-reviews`
- Category: Listings
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-reviews
- Markdown: https://eto.tools/dev/docs/listings/listing-reviews.md

Retrieve reviews for a specific listing.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/reviews`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |
| `limit` | query | `integer` | no | Number of reviews (max 100) Default: `25`. |
| `offset` | query | `integer` | no | Pagination offset Default: `0`. |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/reviews?limit=5"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/reviews?limit=5"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/reviews?limit=5";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/reviews` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.


## GET /api/v1/listings/{listing_id}/inventory

Get listing inventory

- Operation ID: `listing_inventory_get`
- Slug: `listing-inventory-get`
- Category: Listings
- Authentication: API key plus at least one connected store. Etsy needs a seller token for this read but not ownership: Eto uses the token of your store that owns the listing (so SKUs come back), otherwise one of your healthy stores.
- HTML: https://eto.tools/dev/docs/#ep-listing-inventory-get
- Markdown: https://eto.tools/dev/docs/listings/listing-inventory-get.md

Retrieve inventory data (products, offerings, prices, quantities, SKUs and the *_on_property arrays) for a listing.
Etsy needs a seller token for this read. When the listing is in one of your connected stores, Eto uses that store's token, so SKUs come back; for any other listing it uses one of your healthy stores and Etsy returns SKUs as empty strings.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/inventory`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |
| `show_deleted` | query | `boolean` | no | Include deleted products and their offerings. Default false. |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/inventory"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/inventory"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/inventory";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/inventory` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## PUT /api/v1/listings/{listing_id}/inventory

Update listing inventory

- Operation ID: `listing_inventory_update`
- Slug: `listing-inventory-update`
- Category: Listings
- Authentication: API key plus a connected store. Eto uses that store’s Etsy token, so the shop_id must belong to a store connected to your Eto account or the call answers 403 STORE_NOT_CONNECTED.
- HTML: https://eto.tools/dev/docs/#ep-listing-inventory-update
- Markdown: https://eto.tools/dev/docs/listings/listing-inventory-update.md

Update inventory, pricing, and variations for a listing you own. This is the only Etsy call that changes an existing listing's price.
Eto finds the connected store that owns the listing and uses its token. A listing outside your connected stores answers 403 LISTING_NOT_IN_CONNECTED_STORE.
Etsy replaces the whole inventory: send every product back with its sku, offerings (price, quantity, is_enabled) and the price_on_property, quantity_on_property and sku_on_property arrays unchanged. The safe pattern is GET the inventory, change the prices, PUT it all back.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/inventory`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |
| `products` | body | `array` | yes | Every product of the listing, each with sku, property_values and offerings [{price, quantity, is_enabled}]. Price is a decimal (amount / divisor). |
| `price_on_property` | body | `array` | no | Property ids that change price. |
| `quantity_on_property` | body | `array` | no | Property ids that change quantity. |
| `sku_on_property` | body | `array` | no | Property ids that change SKU. |
| `readiness_state_on_property` | body | `array` | no | Property ids that change processing profile. |

### Request

```bash
curl -X PUT -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"products": [...]}' "https://eto.tools/api/v1/listings/1234567890/inventory"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/inventory"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = "{\"products\": [...]}"

response = requests.put(url, headers=headers, data=payload, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/inventory";
const payload = "{\"products\": [...]}";

const response = await fetch(url, {
  method: "PUT",
  body: payload,
  headers: {
    "X-Eto-API-Key": "eto_your_key",
    "Content-Type": "application/json",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/inventory` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## GET · PUT /api/v1/shops/{shop_id}/listings/{listing_id}/inventory

Get or update listing inventory (shop-scoped)

- Operation ID: `shop_listing_inventory_get`
- Slug: `shop-listing-inventory`
- Category: Listings
- Authentication: API key plus a connected store. Eto uses that store’s Etsy token, so the shop_id must belong to a store connected to your Eto account or the call answers 403 STORE_NOT_CONNECTED.
- HTML: https://eto.tools/dev/docs/#ep-shop-listing-inventory
- Markdown: https://eto.tools/dev/docs/listings/shop-listing-inventory.md
- Operation IDs by method: `shop_listing_inventory_get` = GET, `shop_listing_inventory_put` = PUT

Same as GET/PUT /api/v1/listings/{listing_id}/inventory, but the shop is named in the path, so Eto uses that store's token directly and skips finding the listing's shop. Use it when you already know the shop.
PUT replaces the whole inventory: GET it, change prices or quantities, PUT every product back with its sku and the *_on_property arrays unchanged.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/inventory`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |
| `products` | body | `array` | no | PUT only. Every product of the listing, each with sku, property_values and offerings [{price, quantity, is_enabled}]. |
| `price_on_property` | body | `array` | no | PUT only. Property ids that change price. |
| `quantity_on_property` | body | `array` | no | PUT only. Property ids that change quantity. |
| `sku_on_property` | body | `array` | no | PUT only. Property ids that change SKU. |
| `readiness_state_on_property` | body | `array` | no | PUT only. Property ids that change processing profile. |

### Request — GET

```bash
curl -X GET "https://eto.tools/api/v1/shops/12345678/listings/1234567890/inventory" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/inventory"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/inventory";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Request — PUT

```bash
curl -X PUT -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"products": [...], "price_on_property": [], "quantity_on_property": [], "sku_on_property": []}' "https://eto.tools/api/v1/shops/12345678/listings/1234567890/inventory"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/inventory"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = "{\"products\": [...], \"price_on_property\": [], \"quantity_on_property\": [], \"sku_on_property\": []}"

response = requests.put(url, headers=headers, data=payload, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/inventory";
const payload = "{\"products\": [...], \"price_on_property\": [], \"quantity_on_property\": [], \"sku_on_property\": []}";

const response = await fetch(url, {
  method: "PUT",
  body: payload,
  headers: {
    "X-Eto-API-Key": "eto_your_key",
    "Content-Type": "application/json",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/inventory` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## GET /api/v1/listings/{listing_id}/videos

Get listing videos

- Operation ID: `listing_videos`
- Slug: `listing-videos`
- Category: Listings
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-videos
- Markdown: https://eto.tools/dev/docs/listings/listing-videos.md

Retrieve all videos for a specific listing.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/videos`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/videos"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/videos"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/videos";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/videos` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.


## GET /api/v1/listings/{listing_id}/personalization

Get listing personalization

- Operation ID: `listing_personalization_get`
- Slug: `listing-personalization-get`
- Category: Listings
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-personalization-get
- Markdown: https://eto.tools/dev/docs/listings/listing-personalization-get.md

Get the personalization questions for a listing.
Returns {personalization_questions: [...]} with up to 5 questions. Question types: text_input, dropdown, unlabeled_upload, labeled_upload.

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/personalization`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/personalization"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/personalization"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/personalization";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/personalization` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.


## POST /api/v1/listings/{listing_id}/personalization

Set listing personalization

- Operation ID: `listing_personalization_post`
- Slug: `listing-personalization-post`
- Category: Listings
- Authentication: API key plus a connected store. Eto uses that store’s Etsy token, so the shop_id must belong to a store connected to your Eto account or the call answers 403 STORE_NOT_CONNECTED.
- HTML: https://eto.tools/dev/docs/#ep-listing-personalization-post
- Markdown: https://eto.tools/dev/docs/listings/listing-personalization-post.md

Create or replace all personalization questions on a listing you own (up to 5; max 1 file-upload question). Eto finds the connected store that owns the listing, drafts included. See the Listing Builder guide for full field constraints.

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/listings/{listing_id}/personalization`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |
| `personalization_questions` | body | `array` | yes | Questions: [{question_type, question_text, instructions?, required?, max_allowed_characters?, max_allowed_files?, options?}]. |

### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"personalization_questions": [{"question_type": "text_input", "question_text": "Name to engrave", "required": true, "max_allowed_characters": 30}]}' "https://eto.tools/api/v1/listings/1234567890/personalization"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/personalization"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "personalization_questions": [
        {
            "question_type": "text_input",
            "question_text": "Name to engrave",
            "required": True,
            "max_allowed_characters": 30
        }
    ]
}

response = requests.post(url, headers=headers, json=payload, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/personalization";
const payload = {
  "personalization_questions": [
    {
      "question_type": "text_input",
      "question_text": "Name to engrave",
      "required": true,
      "max_allowed_characters": 30
    }
  ]
};

const response = await fetch(url, {
  method: "POST",
  body: JSON.stringify(payload),
  headers: {
    "X-Eto-API-Key": "eto_your_key",
    "Content-Type": "application/json",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/shops/{shop_id}/listings/{listing_id}/personalization` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## DELETE /api/v1/listings/{listing_id}/personalization

Delete listing personalization

- Operation ID: `listing_personalization_delete`
- Slug: `listing-personalization-delete`
- Category: Listings
- Authentication: API key plus a connected store. Eto uses that store’s Etsy token, so the shop_id must belong to a store connected to your Eto account or the call answers 403 STORE_NOT_CONNECTED.
- HTML: https://eto.tools/dev/docs/#ep-listing-personalization-delete
- Markdown: https://eto.tools/dev/docs/listings/listing-personalization-delete.md

Remove personalization from a listing you own. Eto finds the connected store that owns the listing.

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/listings/{listing_id}/personalization`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |

### Request

```bash
curl -X DELETE -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/personalization"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/personalization"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.delete(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/personalization";

const response = await fetch(url, {
  method: "DELETE",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/shops/{shop_id}/listings/{listing_id}/personalization` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## POST · DELETE /api/v1/shops/{shop_id}/listings/{listing_id}/personalization

Set or delete personalization (shop-scoped)

- Operation ID: `shop_listing_personalization_post`
- Slug: `shop-listing-personalization`
- Category: Listings
- Authentication: API key plus a connected store. Eto uses that store’s Etsy token, so the shop_id must belong to a store connected to your Eto account or the call answers 403 STORE_NOT_CONNECTED.
- HTML: https://eto.tools/dev/docs/#ep-shop-listing-personalization
- Markdown: https://eto.tools/dev/docs/listings/shop-listing-personalization.md
- Operation IDs by method: `shop_listing_personalization_post` = POST, `shop_listing_personalization_delete` = DELETE

POST creates or replaces all personalization questions; DELETE removes personalization. Same as the listing-scoped routes, but the shop is named in the path, so Eto skips finding the listing's shop.

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/listings/{listing_id}/personalization`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID |
| `listing_id` | path | `integer` | yes | The Etsy listing ID |
| `personalization_questions` | body | `array` | no | POST only. Questions: [{question_type, question_text, instructions?, required?, max_allowed_characters?, max_allowed_files?, options?}]. |

### Request — POST

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"personalization_questions": [{"question_type": "text_input", "question_text": "Name to engrave", "required": true}]}' "https://eto.tools/api/v1/shops/12345678/listings/1234567890/personalization"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/personalization"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "personalization_questions": [
        {
            "question_type": "text_input",
            "question_text": "Name to engrave",
            "required": True
        }
    ]
}

response = requests.post(url, headers=headers, json=payload, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/personalization";
const payload = {
  "personalization_questions": [
    {
      "question_type": "text_input",
      "question_text": "Name to engrave",
      "required": true
    }
  ]
};

const response = await fetch(url, {
  method: "POST",
  body: JSON.stringify(payload),
  headers: {
    "X-Eto-API-Key": "eto_your_key",
    "Content-Type": "application/json",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Request — DELETE

```bash
curl -X DELETE "https://eto.tools/api/v1/shops/12345678/listings/1234567890/personalization" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/personalization"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.delete(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/personalization";

const response = await fetch(url, {
  method: "DELETE",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/shops/{shop_id}/listings/{listing_id}/personalization` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## GET /api/v1/listings/batch

Batch get listings

- Operation ID: `listings_batch`
- Slug: `listings-batch`
- Category: Listings
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listings-batch
- Markdown: https://eto.tools/dev/docs/listings/listings-batch.md

Retrieve up to 100 listings in a single request. API key only: no store needs to be connected.
With includes=BuyerPrice and buyer_country (e.g. GB), each listing carries buyer_price: the buyer-facing price with VAT, UK inclusive shipping and any active promotion. Etsy only returns buyer_price on this route.

Proxied to the Etsy API at `/v3/application/listings/batch`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_ids` | query | `string` | yes | Comma-separated listing IDs (max 100) |
| `includes` | query | `string` | no | Comma-separated associations: Shop, Images, User, Translations, Videos, Personalization, BuyerPrice |
| `buyer_country` | query | `string` | no | ISO 3166-1 alpha-2 country (e.g. GB, DE) for buyer_price. Required with includes=BuyerPrice. Does not filter listings. |
| `currency` | query | `string` | no | ISO 4217 currency (e.g. EUR) to add converted_price to each listing. |
| `legacy` | query | `boolean` | no | Enable the newer processing-profile fields. |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/batch?listing_ids=123,456,789&includes=BuyerPrice&buyer_country=GB"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/batch?listing_ids=123,456,789&includes=BuyerPrice&buyer_country=GB"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/batch?listing_ids=123,456,789&includes=BuyerPrice&buyer_country=GB";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/batch` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.


## GET /api/v1/listings/batch/inventory

Batch get listing inventory

- Operation ID: `listings_batch_inventory`
- Slug: `listings-batch-inventory`
- Category: Listings
- Authentication: API key plus at least one connected store. Etsy needs a seller token for this read but not ownership: Eto uses the token of your store that owns the listing (so SKUs come back), otherwise one of your healthy stores.
- HTML: https://eto.tools/dev/docs/#ep-listings-batch-inventory
- Markdown: https://eto.tools/dev/docs/listings/listings-batch-inventory.md

Retrieve the inventory record (products, offerings, prices, quantities, SKUs) for up to 100 listings in one call. If any listing_id does not exist, Etsy fails the whole request with 404.
Etsy only returns SKUs for listings in the shop whose token is used. Eto uses the connected store that owns most of the requested listings; pass shop_id to choose it. For SKUs on every listing, batch the ids by shop.

Proxied to the Etsy API at `/v3/application/listings/batch/inventory`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_ids` | query | `string` | yes | Comma-separated listing IDs (max 100) |
| `shop_id` | query | `integer` | no | Optional. The connected shop whose token to use (SKUs come back for its listings). |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/batch/inventory?listing_ids=123,456,789&shop_id=12345678"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/batch/inventory?listing_ids=123,456,789&shop_id=12345678"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/batch/inventory?listing_ids=123,456,789&shop_id=12345678";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/batch/inventory` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.


## GET /api/v1/listings/{listing_id}/products/{product_id}/offerings/{offering_id}

Get a listing offering

- Operation ID: `listing_offering`
- Slug: `listing-offering`
- Category: Listings
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-offering
- Markdown: https://eto.tools/dev/docs/listings/listing-offering.md

Get pricing and availability for a specific product variation offering. Etsy docs: "Listing Offering".

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/products/{product_id}/offerings/{offering_id}`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The listing ID |
| `product_id` | path | `integer` | yes | The product variant ID |
| `offering_id` | path | `integer` | yes | The offering ID |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/products/555666777/offerings/888999000"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/products/555666777/offerings/888999000"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/products/555666777/offerings/888999000";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/products/{product_id}/offerings/{offering_id}` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.


## GET /api/v1/listings/{listing_id}/inventory/products/{product_id}

Get a product variant

- Operation ID: `listing_product`
- Slug: `listing-product`
- Category: Listings
- Authentication: API key plus at least one connected store. Etsy needs a seller token for this read but not ownership: Eto uses the token of your store that owns the listing (so SKUs come back), otherwise one of your healthy stores.
- HTML: https://eto.tools/dev/docs/#ep-listing-product
- Markdown: https://eto.tools/dev/docs/listings/listing-product.md

Get a specific product variant (size/color combination) and its offerings. Etsy docs: "Listing Product".

Proxied to the Etsy API at `/v3/application/listings/{listing_id}/inventory/products/{product_id}`. Fields Etsy returns are passed through untouched.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `listing_id` | path | `integer` | yes | The listing ID |
| `product_id` | path | `integer` | yes | The product variant ID |

### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/1234567890/inventory/products/555666777"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/inventory/products/555666777"
headers = {"X-Eto-API-Key": "eto_your_key"}

response = requests.get(url, headers=headers, timeout=120)
response.raise_for_status()
print(response.json())
```

```javascript
const url = "https://eto.tools/api/v1/listings/1234567890/inventory/products/555666777";

const response = await fetch(url, {
  method: "GET",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
if (!response.ok) throw new Error(`Eto API ${response.status}`);
const data = await response.json();
```

### Response

`application/json`, passed through from Etsy’s `/v3/application/listings/{listing_id}/inventory/products/{product_id}` verbatim. The field-by-field data model is in the [v2 preview reference](https://eto.tools/eto-dev-api-v2/), which documents these same routes alongside the whole Etsy schema.

### Errors

Failures answer with the shared error body described in the Errors section: `{"error": {"code", "status", "message", "hint", "docs"}}`. Branch on `code`, not on the message.

This endpoint can additionally return `STORE_NOT_CONNECTED` (403) when the shop is not linked to your account, and `STORE_TOKEN_EXPIRED` (503) when its Etsy authorisation has lapsed and needs reconnecting.
