# Create a listing

> Eto API reference for `POST /api/v1/listings/create`. Base URL `https://eto.tools/api/v1/`. Authenticate with the `X-Eto-API-Key` header.

Part of the [Eto API reference](https://eto.tools/dev/docs/). The full reference in one file is at [llms-full.txt](https://eto.tools/dev/docs/llms-full.txt); the machine-readable schema is at [openapi.json](https://eto.tools/dev/docs/openapi.json).

## POST /api/v1/listings/create

Create a listing

- Operation ID: `listing_create`
- Slug: `listing-create`
- Category: Listing Builder
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-listing-create
- Markdown: https://eto.tools/dev/docs/listing-builder/listing-create.md

Create a complete Etsy listing with a single JSON payload. Handles title, description, images (URLs or eto-upload:// tokens from POST /api/v1/uploads), variations, pricing, tags, materials, personalization, and multi-shop publishing. ETO handles all Etsy API complexity: variation property ordering, Cartesian product completion, price_on_property detection, SKU truncation, image ordering, and inventory updates.

state="draft"   → saves to ETO only. No Etsy API calls. Returns immediately.
state="publish" → saves to ETO and creates a draft listing on Etsy. Blocking: the request holds open until Etsy has accepted the listing and all images/videos/files are attached (typically 5–15s per shop). Returns the final results inline.
state="active"  → same as publish + activates each listing on Etsy. Costs $0.20 USD per listing per shop.

You can only publish to Etsy shops connected to your ETO account. Use GET /api/v1/stores to list them.

For backwards compatibility, the response still includes job_id and poll_url so existing clients that poll GET /api/v1/listings/create/{job_id} continue to work — the job will be in "completed" state on the first poll.

Validation errors (400 VALIDATION_FAILED):
  • images[N].url / digital_files[N].url / videos[N].url — "Uploaded file is no longer available on the server. Re-upload via POST /api/v1/uploads and use the new url."
    Fires when the eto-upload:// path no longer resolves on disk. Re-upload and retry.
  • shops[N].shop_section_id — "Section X not found in shop Y."
    Fires when the section ID does not belong to the shop. Call GET /api/v1/stores/{shop_id}/shop-sections/live to fetch the current sections.

All preflight validation runs before any ETO row or Etsy draft is created — a 400 means nothing was persisted and retrying with a fixed payload is safe.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `state` | body | `string` | no | "draft" saves to ETO only (instant, zero Etsy calls). "publish" saves to ETO and creates a draft listing on Etsy (blocking, 5–15s per shop). "active" same as publish + activates on Etsy (blocking, costs $0.20 USD per listing per shop). Default: `draft`. One of: `draft`, `publish`, `active`. |
| `shops[].shop_id` | body | `integer` | yes | The unique positive non-zero numeric ID for an Etsy Shop. Must be connected to your ETO account. Use GET /api/v1/stores to list connected shops. Constraints: >= 1. |
| `shops[].price` | body | `number` | no | Per-shop price override in the shop's native currency. If omitted, uses listing.price. Constraints: >= 0.20. Nullable. |
| `shops[].shipping_profile_id` | body | `integer` | no | The numeric ID of the shipping profile associated with the listing. Required when listing_type is physical. Get the latest IDs from GET /api/v1/stores/{shop_id}/shipping-profiles/live. Constraints: >= 1. Nullable. |
| `shops[].return_policy_id` | body | `integer` | no | The numeric ID of the return policy. Required for physical listings. Get the latest IDs from GET /api/v1/stores/{shop_id}/return-policies/live. Constraints: >= 1. Nullable. |
| `shops[].processing_profile_id` | body | `integer` | no | The numeric ID of the processing/readiness state profile. Controls processing time display on the listing. Get the latest IDs from GET /api/v1/stores/{shop_id}/processing-profiles/live. Constraints: >= 1. Nullable. |
| `shops[].shop_section_id` | body | `integer` | no | The numeric ID of the shop section to organize the listing under. Get the latest IDs from GET /api/v1/stores/{shop_id}/shop-sections/live. Constraints: >= 1. Nullable. |
| `shops[].production_partner_ids` | body | `array` | no | An array of unique IDs of production partners for this listing. Get IDs from POST /api/v1/stores/{shop_id}/sync. Nullable. |
| `listing.title` | body | `string` | yes | The listing's title string. Valid title strings contain only letters, numbers, punctuation marks, mathematical symbols, whitespace characters, ™, ©, and ®. You can only use the %, :, & and + characters once each. Constraints: max 140 chars. Pattern: `/[^\p{L}\p{Nd}\p{P}\p{Sm}\p{Zs}™©®]/u`. |
| `listing.description` | body | `string` | yes | A description string of the product for sale in the listing. Newlines are rendered by Etsy. HTML tags are stripped. |
| `listing.listing_type` | body | `string` | yes | An enumerated type string that indicates whether the listing is a physical product or a digital download. One of: `physical`, `digital`. |
| `listing.taxonomy_id` | body | `integer` | yes | The numerical taxonomy ID of the listing. Use GET /api/v1/categories/{id}/listing-schema to find valid IDs and discover required category attributes and variation properties. Constraints: >= 1. |
| `listing.price` | body | `number` | no | The positive non-zero price of the product. Required unless variations define per-variant prices. Note: The price is the minimum possible price. Variation offerings can set different prices per combination. Constraints: >= 0.20. |
| `listing.quantity` | body | `integer` | no | The positive non-zero number of products available for purchase. Required unless variations define per-variant quantities. Note: For variation listings, this is overridden by per-offering quantities. Constraints: 1-999. |
| `listing.who_made` | body | `string` | no | An enumerated string indicating who made the product. Helps buyers locate the listing under the Handmade heading. Requires 'is_supply' and 'when_made'. Default: `i_did`. One of: `i_did`, `someone_else`, `collective`. |
| `listing.when_made` | body | `string` | no | An enumerated string for the era in which the maker made the product in this listing. Helps buyers locate the listing under the Vintage heading. Requires 'is_supply' and 'who_made'. Default: `2020_2026`. One of: `made_to_order`, `2020_2026`, `2010_2019`, `2007_2009`, `before_2007`, `2000_2006`, `1990s`, `1980s`, `1970s`, `1960s`, `1950s`, `1940s`, `1930s`, `1920s`, `1910s`, `1900s`. |
| `listing.tags` | body | `array` | no | A list of tag strings for the listing. Valid tag strings contain only letters, numbers, whitespace characters, -, ', ™, ©, and ®. Default value is null. Constraints: max 13 items, each max 20 chars. Pattern: `/[^\p{L}\p{Nd}\p{Zs}\-'™©®]/u`. Nullable. |
| `listing.materials` | body | `array` | no | A list of material strings for materials used in the product. Valid materials strings contain only letters, numbers, and whitespace characters. Default value is null. Constraints: max 13 items. Pattern: `/[^\p{L}\p{Nd}\p{Zs}]/u`. Nullable. |
| `listing.styles` | body | `array` | no | An array of style strings for this listing, each of which is free-form text string such as "Formal", or "Steampunk". Valid style strings contain only letters, numbers, and whitespace characters. Default value is null. Constraints: max 2 items. Pattern: `/[^\p{L}\p{Nd}\p{Zs}]/u`. Nullable. |
| `listing.sku` | body | `string` | no | Optional internal identifier. First 32 characters are sent to Etsy (Etsy's limit). The full SKU (up to 512 chars) is stored in ETO for order fulfillment and can be retrieved via the ETO API. Constraints: max 512 chars (32 to Etsy). |
| `listing.is_supply` | body | `boolean` | no | When true, tags the listing as a supply product, else indicates that it's a finished product. Helps buyers locate the listing under the Supplies heading. Requires 'who_made' and 'when_made'. Default: `false`. |
| `listing.is_customizable` | body | `boolean` | no | When true, a buyer may contact the seller for a customized order. The default value is true when a shop accepts custom orders. Does not apply to shops that do not accept custom orders. |
| `listing.is_taxable` | body | `boolean` | no | When true, applicable shop tax rates apply to this listing at checkout. |
| `listing.should_auto_renew` | body | `boolean` | no | When true, renews a listing for four months upon expiration. |
| `listing.item_weight` | body | `number` | no | The numeric weight of the product measured in units set in 'item_weight_unit'. Default value is null. If set, the value must be greater than 0. Constraints: > 0. Nullable. |
| `listing.item_weight_unit` | body | `string` | no | A string defining the units used to measure the weight of the product. Default value is null. One of: `oz`, `lb`, `g`, `kg`. Nullable. |
| `listing.item_length` | body | `number` | no | The numeric length of the product measured in units set in 'item_dimensions_unit'. Default value is null. If set, the value must be greater than 0. Constraints: > 0. Nullable. |
| `listing.item_width` | body | `number` | no | The numeric width of the product measured in units set in 'item_dimensions_unit'. Default value is null. If set, the value must be greater than 0. Constraints: > 0. Nullable. |
| `listing.item_height` | body | `number` | no | The numeric height of the product measured in units set in 'item_dimensions_unit'. Default value is null. If set, the value must be greater than 0. Constraints: > 0. Nullable. |
| `listing.item_dimensions_unit` | body | `string` | no | A string defining the units used to measure the dimensions of the product. Default value is null. One of: `in`, `ft`, `mm`, `cm`, `m`, `yd`, `inches`. Nullable. |
| `listing.processing_min` | body | `integer` | no | The minimum number of days required to process this listing. Default value is null. Nullable. |
| `listing.processing_max` | body | `integer` | no | The maximum number of days required to process this listing. Default value is null. Nullable. |
| `images[].url` | body | `string` | yes | A publicly accessible URL to the image file. ETO downloads the image server-side and uploads it to Etsy. Supported formats: JPG, PNG, GIF. Max 100MB per image. Constraints: min 1, max 10 images. |
| `images[].rank` | body | `integer` | no | Position in the listing image gallery. Rank 1 is the primary/leftmost photo shown to buyers. If omitted, images are ordered by their position in the array. Constraints: 1-10. |
| `digital_files[].url` | body | `string` | no | A publicly accessible URL to the digital file. Required for listing_type=digital. ETO downloads the file server-side and uploads it to Etsy. Max 100MB per file. Supported types: .bmp, .doc, .gif, .jpeg, .jpg, .mobi, .mov, .mp3, .mpeg, .pdf, .png, .psp, .rtf, .stl, .txt, .zip, .ePUB, .iBook. Constraints: min 1, max 5 files (for digital listings). |
| `digital_files[].name` | body | `string` | no | The display name for the digital file shown to buyers after purchase. Letters, numbers, periods, hyphens, and underscores only. No spaces or parentheses. Constraints: 3-70 chars. Pattern: `/^[a-zA-Z0-9._-]+$/`. |
| `videos[].url` | body | `string` | no | A publicly accessible URL to the video file. ETO downloads the video server-side and uploads it to Etsy. MP4 or MOV format, max 15 seconds, no audio. Max 100MB. Constraints: max 1 video per listing. Nullable. |
| `personalization.enabled` | body | `boolean` | no | When true, enables personalization for this listing. ETO uses the modern Etsy personalization API to set this up after listing creation. Default: `false`. |
| `personalization.is_required` | body | `boolean` | no | When true, the buyer must enter personalization text before purchasing. Only applies when personalization.enabled is true. Default: `false`. |
| `personalization.instructions` | body | `string` | no | Instructions shown to the buyer at checkout describing what personalization to enter (e.g. 'Enter up to 10 characters for monogram'). Only applies when personalization.enabled is true. Constraints: max 256 chars. |
| `personalization.max_chars` | body | `integer` | no | The maximum character count for the buyer's personalization message. Only applies when personalization.enabled is true. Constraints: 1-1024. |
| `category_attributes` | body | `object` | no | Category-specific listing attributes. Keys are property_id strings. Values are objects with value_ids, values, and optional scale_id. Use GET /api/v1/categories/{id}/listing-schema to discover which attributes exist, which are required, and their valid values for a given category. |
| `category_attributes.{property_id}.value_ids` | body | `array` | no | The Etsy value IDs for the selected attribute values. Get valid IDs from the listing-schema endpoint possible_values array. |
| `category_attributes.{property_id}.values` | body | `array` | no | Human-readable attribute value strings corresponding to each value_id. |
| `category_attributes.{property_id}.scale_id` | body | `integer` | no | Scale ID for properties that use scales (e.g. alpha sizing). Get valid scale IDs from the listing-schema endpoint scales array. Nullable. |
| `variations.properties` | body | `array` | no | Array of variation property definitions. Etsy allows at most 2 variation properties per listing. Use GET /api/v1/categories/{id}/listing-schema variation_properties to discover available properties for the chosen category. Constraints: max 2 items. |
| `variations.properties[].property_id` | body | `integer` | yes | The Etsy property ID from the listing-schema variation_properties (e.g. 200 for Primary color, 100 for Size, 513 for Style). |
| `variations.properties[].name` | body | `string` | yes | Human-readable property name (e.g. 'Color', 'Size'). This name (lowercased) is used as the key in each offering to specify the value. |
| `variations.properties[].scale_id` | body | `integer` | no | Scale ID for sized properties (e.g. Alpha sizing: XS, S, M, L, XL). Get valid scale IDs from the listing-schema endpoint. Nullable. |
| `variations.properties[].values` | body | `array` | yes | All possible values for this property (e.g. ['Black', 'Brown', 'Tan'] for Color). Parentheses characters () are not allowed in values. |
| `variations.offerings` | body | `array` | no | Array of offerings, one per variation combination. Each offering uses the lowercased property name as a key to specify which value it represents. ETO auto-detects which properties affect price/quantity and builds the Etsy inventory payload accordingly. |
| `variations.offerings[].{property_name}` | body | `string` | yes | The value for this property in this offering. The key is the lowercased property name (e.g. 'color': 'Black', 'size': 'S'). Must match one of the values listed in variations.properties[].values. |
| `variations.offerings[].price` | body | `number` | no | Price for this specific variation combination. If all offerings have the same price, ETO automatically tells Etsy that price does not vary by property. If prices differ, ETO detects which property causes the difference. Constraints: >= 0.20. |
| `variations.offerings[].quantity` | body | `integer` | no | Stock quantity for this specific variation combination. Set to 0 along with enabled=false to create the combination but hide it from the listing. Constraints: 0-999. |
| `variations.offerings[].sku` | body | `string` | no | Per-variant SKU identifier. First 32 characters are sent to Etsy, full SKU (up to 512 chars) is stored in ETO. Must be unique across all offerings if provided. Constraints: max 512 chars (32 to Etsy). Nullable. |
| `variations.offerings[].enabled` | body | `boolean` | no | When false, the offering is created but hidden from the listing. Etsy receives is_enabled=false and quantity=0. Useful for pre-creating combinations that are temporarily out of stock. Default: `true`. |
| `variations.offerings[].processing_profile_id` | body | `integer` | no | Per-variant processing/readiness profile ID. If different values are set across offerings, ETO automatically sets readiness_state_on_property. If omitted, the shop-level processing_profile_id applies. Constraints: >= 1. Nullable. |
| `variations.variation_images.property` | body | `string` | no | The name of the variation property whose values should be linked to specific listing images (e.g. 'color'). Only one property can have variation images per listing (Etsy constraint). Case-insensitive match against variations.properties[].name. |
| `variations.variation_images.mapping` | body | `object` | no | Maps property values to image indices. Keys are value names (e.g. 'Black'), values are 0-based indices into the images[] array. Example: {"Black": 0, "Brown": 1, "Tan": 2}. If a value references an out-of-bounds index, validation returns an error. |

### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" \
  "https://eto.tools/api/v1/listings/create" \
  -d '{
  "state": "draft",
  "shops": [{"shop_id": 12345678, "shipping_profile_id": 111222, "return_policy_id": 333444, "price": 24.99}],
  "listing": {
    "title": "Custom Leather Wallet - Personalized Gift for Him",
    "description": "Handmade genuine leather wallet with optional monogram...",
    "listing_type": "physical",
    "taxonomy_id": 2078,
    "price": 24.99,
    "quantity": 50,
    "who_made": "i_did",
    "when_made": "2020_2026",
    "tags": ["leather wallet", "personalized gift", "groomsmen gift"],
    "materials": ["leather", "thread"],
    "sku": "WALLET-CUSTOM-001",
    "item_weight": 4.5,
    "item_weight_unit": "oz"
  },
  "images": [
    {"url": "https://example.com/wallet-front.jpg", "rank": 1},
    {"url": "https://example.com/wallet-back.jpg", "rank": 2}
  ],
  "variations": {
    "properties": [
      {"property_id": 200, "name": "Color", "values": ["Black", "Brown", "Tan"]}
    ],
    "offerings": [
      {"color": "Black", "price": 24.99, "quantity": 50, "sku": "W-BLK"},
      {"color": "Brown", "price": 24.99, "quantity": 50, "sku": "W-BRN"},
      {"color": "Tan", "price": 27.99, "quantity": 30, "sku": "W-TAN"}
    ],
    "variation_images": {"property": "color", "mapping": {"Black": 0, "Brown": 1}}
  }
}'
```

```python
import requests

url = "https://eto.tools/api/v1/listings/create"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "state": "draft",
    "shops": [
        {
            "shop_id": 12345678,
            "shipping_profile_id": 111222,
            "return_policy_id": 333444,
            "price": 24.99
        }
    ],
    "listing": {
        "title": "Custom Leather Wallet - Personalized Gift for Him",
        "description": "Handmade genuine leather wallet with optional monogram...",
        "listing_type": "physical",
        "taxonomy_id": 2078,
        "price": 24.99,
        "quantity": 50,
        "who_made": "i_did",
        "when_made": "2020_2026",
        "tags": [
            "leather wallet",
            "personalized gift",
            "groomsmen gift"
        ],
        "materials": [
            "leather",
            "thread"
        ],
        "sku": "WALLET-CUSTOM-001",
        "item_weight": 4.5,
        "item_weight_unit": "oz"
    },
    "images": [
        {
            "url": "https://example.com/wallet-front.jpg",
            "rank": 1
        },
        {
            "url": "https://example.com/wallet-back.jpg",
            "rank": 2
        }
    ],
    "variations": {
        "properties": [
            {
                "property_id": 200,
                "name": "Color",
                "values": [
                    "Black",
                    "Brown",
                    "Tan"
                ]
            }
        ],
        "offerings": [
            {
                "color": "Black",
                "price": 24.99,
                "quantity": 50,
                "sku": "W-BLK"
            },
            {
                "color": "Brown",
                "price": 24.99,
                "quantity": 50,
                "sku": "W-BRN"
            },
            {
                "color": "Tan",
                "price": 27.99,
                "quantity": 30,
                "sku": "W-TAN"
            }
        ],
        "variation_images": {
            "property": "color",
            "mapping": {
                "Black": 0,
                "Brown": 1
            }
        }
    }
}

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/create";
const payload = {
  "state": "draft",
  "shops": [
    {
      "shop_id": 12345678,
      "shipping_profile_id": 111222,
      "return_policy_id": 333444,
      "price": 24.99
    }
  ],
  "listing": {
    "title": "Custom Leather Wallet - Personalized Gift for Him",
    "description": "Handmade genuine leather wallet with optional monogram...",
    "listing_type": "physical",
    "taxonomy_id": 2078,
    "price": 24.99,
    "quantity": 50,
    "who_made": "i_did",
    "when_made": "2020_2026",
    "tags": [
      "leather wallet",
      "personalized gift",
      "groomsmen gift"
    ],
    "materials": [
      "leather",
      "thread"
    ],
    "sku": "WALLET-CUSTOM-001",
    "item_weight": 4.5,
    "item_weight_unit": "oz"
  },
  "images": [
    {
      "url": "https://example.com/wallet-front.jpg",
      "rank": 1
    },
    {
      "url": "https://example.com/wallet-back.jpg",
      "rank": 2
    }
  ],
  "variations": {
    "properties": [
      {
        "property_id": 200,
        "name": "Color",
        "values": [
          "Black",
          "Brown",
          "Tan"
        ]
      }
    ],
    "offerings": [
      {
        "color": "Black",
        "price": 24.99,
        "quantity": 50,
        "sku": "W-BLK"
      },
      {
        "color": "Brown",
        "price": 24.99,
        "quantity": 50,
        "sku": "W-BRN"
      },
      {
        "color": "Tan",
        "price": 27.99,
        "quantity": 30,
        "sku": "W-TAN"
      }
    ],
    "variation_images": {
      "property": "color",
      "mapping": {
        "Black": 0,
        "Brown": 1
      }
    }
  }
};

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

```
# state="draft" — 201 Created
{
  "listing_pk": 1173,
  "status": "draft",
  "message": "Listing saved as draft in ETO. Open it in the ETO dashboard to review and publish.",
  "dashboard_url": "/dashboard/single-research/1173/"
}

# state="publish" or "active" — 200 OK, synchronous
{
  "job_id": "lcj_aDzKwnFW5sYHewZaPTduRw",
  "listing_pk": 1173,
  "status": "completed",
  "poll_url": "/api/v1/listings/create/lcj_aDzKwnFW5sYHewZaPTduRw",
  "dashboard_url": "/dashboard/single-research/1173/",
  "shops_count": 1,
  "results": [
    {
      "shop_id": "53081804",
      "status": "ok",
      "listing_id": 4489813117,
      "listing_url": "https://www.etsy.com/listing/4489813117",
      "currency_code": "GBP",
      "price": 24.99,
      "images": [
        {"listing_image_id": 5523110099001, "url_fullxfull": "https://i.etsystatic.com/...", "rank": 1},
        {"listing_image_id": 5523110099002, "url_fullxfull": "https://i.etsystatic.com/...", "rank": 2}
      ],
      "videos": []
    }
  ]
}

# For state="active", each succeeded shop also includes:
#   "activated": true, "activation_cost_usd": 0.20

# On per-shop failure (other shops may still succeed):
#   {"shop_id": "...", "status": "error", "error": "<reason>", "etsy_status": 400, "details": {...}}
```

### 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.
