# Eto API

> The Eto API is a REST/JSON interface to Etsy marketplace data and to the Eto seller platform: search and research listings, build and publish them, read orders, finance and traffic for the shops connected to your account, and generate listing copy. 127 operations across 19 categories, one API key, `https://eto.tools/api/v1/`.

This file is the whole reference. Read the sections in order for the rules that apply to every call (auth, limits, pagination, errors), then jump to the category you need. Every endpoint below lists its operation ID, its parameters, a runnable request in three languages, and the errors it can return.

Written for automated readers as much as for people: paths, headers, status codes and field names are exact, examples are runnable once you substitute your key, and nothing is described only in a screenshot.

The last chapter, **Where everything is**, maps the Eto app itself: every page, every section and every feature with the exact click path to reach it. Read it when the question is about the interface rather than the API.

## Machine-readable versions of this document

| URL | What it is |
| --- | --- |
| `https://eto.tools/dev/docs/llms.txt` | Short index of every endpoint, in the llms.txt format. |
| `https://eto.tools/dev/docs/llms-full.txt` | This entire reference as one Markdown file. |
| `https://eto.tools/dev/docs/index.md` | The same document, served as the Markdown twin of the HTML page. |
| `https://eto.tools/dev/docs/openapi.json` | OpenAPI 3.1 description of every operation. |
| `https://eto.tools/dev/docs/openapi.yaml` | The same document in YAML. |
| `https://eto.tools/dev/docs/<category>/<slug>.md` | One endpoint on its own. |
| `https://eto.tools/dev/docs/where-everything-is.md` | Where everything is: every page, feature and control in the app, with its UI path. |
| `https://eto.tools/dev/docs/feature-map.json` | The same feature map as JSON. |
| `https://mcp.eto.tools/mcp` | MCP server, one tool per operation. |

The HTML page also answers `Accept: text/markdown` (and `?format=md`) with the Markdown version. Anchors on the HTML page are `#ep-` plus the endpoint slug, so the endpoint with slug `listings-search` is at `https://eto.tools/dev/docs/#ep-listings-search`.

## Start here

1. Generate a key in the [API console](https://eto.tools/dashboard/api/). The API and the MCP server are an Enterprise feature; a key on any other plan is refused with `INVALID_API_KEY`.
2. Send it on every request as `X-Eto-API-Key: eto_your_key`.
3. List the shops the key can reach, and use those ids everywhere `{shop_id}` appears.

```bash
curl "https://eto.tools/api/v1/stores" \
  -H "X-Eto-API-Key: eto_your_key"
```

## Base URL

Every path in this document is relative to:

```
https://eto.tools/api/v1/
```

`v1` is the only version that answers requests. Endpoints are added, never removed: a path that answers today keeps answering.

## Authentication

Two credentials are accepted on every endpoint and resolve to the same account:

| Header | Value | Used by |
| --- | --- | --- |
| `X-Eto-API-Key` | `eto_live_...` from the API console | Direct API calls |
| `Authorization` | `Bearer <token>` from the OAuth 2.1 flow | MCP clients that sign in with Eto |

There is no cookie or session fallback. A missing, invalid or revoked credential answers `401 INVALID_API_KEY`.

Endpoints marked *API key plus a connected store* additionally need the Etsy shop connected to your Eto account, because Eto calls Etsy with that store’s token.

## Rate limits

| Limit | Window | On breach |
| --- | --- | --- |
| 2 requests | per second (sliding) | `429 RATE_LIMIT_SECOND` with `Retry-After: 1` |
| 5,000 requests | per day, resets midnight UTC | `429 RATE_LIMIT_DAILY` with `Retry-After` in seconds |

Limits are counted per account, not per key, so rotating a key does not reset them. Every response carries the current state:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit-Second` | The per-second ceiling (always 2). |
| `X-RateLimit-Remaining-Second` | Requests left in the current second. |
| `X-RateLimit-Limit-Day` | The daily ceiling (always 5000). |
| `X-RateLimit-Remaining-Day` | Requests left today. Resets at midnight UTC. |
| `Retry-After` | On a 429 only: seconds to wait before retrying. |

On a 429, wait the number of seconds in `Retry-After` before retrying, and back off exponentially if it keeps happening. `Retry-After` always takes precedence over anything inferred from the other headers.

## Pagination

List endpoints take `limit` and `offset` query parameters. Both are optional; each endpoint documents its own default and ceiling, and a `limit` above the ceiling is clamped rather than rejected. Responses carry the total beside the page, so stop when `offset + len(results) >= count`.

Etsy passthrough endpoints name the array `results` and the total `count`. Eto’s own list endpoints (orders, webhook deliveries) name the array after the resource — `orders`, `deliveries` — and return `total`. Each endpoint’s response example below shows the exact field names.

## Money, ids and dates

- Money from Etsy is `{"amount": 2999, "divisor": 100, "currency_code": "USD"}`; divide `amount` by `divisor`. Money computed by Eto is an integer field whose name ends in `_cents`.
- `shop_id`, `listing_id` and `receipt_id` are Etsy’s own ids. Get `shop_id` from `GET /api/v1/stores`.
- Date filters accept a named `period` (`today`, `yesterday`, `last_7_days`, `last_30_days`, `this_month`, `last_month`) or `start_date`/`end_date` as `YYYY-MM-DD`, inclusive at both ends, resolved in the shop’s timezone. Prefer those over computing Unix timestamps yourself.

## Errors

Every failure answers with the HTTP status and one body of the same shape:

```json
{
  "error": {
    "code": "INVALID_API_KEY",
    "status": 401,
    "message": "Your API key is missing, invalid, or has been revoked.",
    "hint": "Generate a new key in the API console at eto.tools/dashboard/api.",
    "docs": "https://eto.tools/dev/docs#authentication"
  }
}
```

`code` is the stable identifier to branch on; `message` and `hint` are for humans and may be reworded. Three codes add one field: `VALIDATION_FAILED` adds `fields` with the per-field problems, `UPSTREAM_ERROR` adds `upstream` with Etsy’s own response, and the two rate-limit codes set a `Retry-After` header.

### Every code the API returns

| Code | Status | Meaning | Fix |
| --- | --- | --- | --- |
| `INVALID_API_KEY` | 401 | Your API key is missing, invalid, or has been revoked. | Generate a new key in the API console at eto.tools/dashboard/api. |
| `STORE_NOT_CONNECTED` | 403 | You don't have access to this store. | Connect the store at eto.tools/dashboard first. |
| `STORE_TOKEN_EXPIRED` | 503 | Store authorization has expired. | Reconnect your store at eto.tools/dashboard. |
| `ENDPOINT_NOT_FOUND` | 404 | This endpoint doesn't exist. | Check the path and method against the docs. |
| `METHOD_NOT_ALLOWED` | 405 | This HTTP method isn't supported on this endpoint. | Allowed methods: the methods this path accepts |
| `RATE_LIMIT_SECOND` | 429 | Rate limit exceeded (2 requests/second). | Wait the number of seconds in the Retry-After headers before retrying. |
| `RATE_LIMIT_DAILY` | 429 | Daily request limit reached (5,000/day). | Limit resets at midnight UTC. |
| `UPSTREAM_ERROR` | 502 | The upstream service returned an error. | Check the upstream field for the marketplace’s own response. |
| `INVALID_REQUEST` | 400 | Request validation failed. | the specific reason, filled in per request |
| `AI_CREDENTIALS_MISSING` | 403 | No AI credentials configured. | Add your Gemini API key at eto.tools/settings. |
| `AI_GENERATION_FAILED` | 502 | Image/text generation failed. | the specific reason, filled in per request |
| `SESSION_NOT_FOUND` | 404 | Research session not found. | Check the session ID. |
| `AGENT_BUSY` | 409 | An agent process is already running for this session. | Wait for it to complete or cancel it. |
| `VALIDATION_FAILED` | 400 | Request validation failed. | Check the fields object for specific issues. |
| `JOB_NOT_FOUND` | 404 | Listing creation job not found. | Check the job_id. Jobs expire after 24 hours. |
| `DELIVERY_NOT_FOUND` | 404 | Webhook delivery not found. | Check the delivery id. List recent deliveries with GET /api/v1/webhooks/deliveries. |
| `TAXONOMY_INVALID` | 400 | The provided taxonomy_id is not a valid Etsy category. | Use GET /api/v1/categories/{id}/listing-schema to find valid categories. |
| `STORE_NOT_OWNED` | 403 | One or more shop_ids are not connected to your ETO account. | Use GET /api/v1/stores to see your connected shops. Connect new stores at eto.tools/dashboard. |
| `FINANCE_NO_DATA` | 404 | No finance data available for this store in the requested range. | Visit the Eto Finance dashboard to sync your store data, or adjust your date range. |
| `ACTIVATION_COST_WARNING` | 200 | Listing activation costs $0.20 USD per listing per shop. | Set state="draft" (default) to avoid charges. |

`ACTIVATION_COST_WARNING` is the one entry that is not a failure: it is returned alongside a 200 when a request activates listings, so the cost is never a surprise.

## Connecting a store

Endpoints that read or write shop data need that Etsy shop connected to your Eto account. Connect one at https://eto.tools/dashboard/stores/, then read the ids with `GET /api/v1/stores`. Calling a shop you have not connected answers `403 STORE_NOT_CONNECTED`.

## Webhooks

Rather than polling for orders, Eto can POST order events to a URL you choose. Configure the URL, the events and the stores in the API console; the signing secret is shown there too.

| Event | Fires when |
| --- | --- |
| `order.paid` | The buyer completed payment for the order. |
| `order.shipped` | Shipping information was created, or the order was marked shipped. |
| `order.canceled` | The seller canceled the order. |
| `order.delivered` | The order was marked delivered. |

| Request header | Meaning |
| --- | --- |
| `X-Eto-Event` | The event type, e.g. order.paid. |
| `X-Eto-Webhook-Id` | Unique id for this delivery. Use it to de-duplicate retries. |
| `X-Eto-Timestamp` | Unix seconds when the delivery was sent. |
| `X-Eto-Signature` | v1,<base64 HMAC-SHA256> over the raw body. Optional integrity check. |

The body is JSON: `event`, `event_description`, `event_id`, `sent_at`, `shop` and a full `order` object. De-duplicate on `X-Eto-Webhook-Id`, and answer 2xx quickly — a non-2xx is retried. List and re-send past deliveries with the webhook endpoints below.

## Calling Eto from an AI client (MCP)

The same account is reachable over the Model Context Protocol at `https://mcp.eto.tools/mcp`. It is a thin layer over the endpoints in this document: same key, same account, same limits, one tool per endpoint named after the operation ID.

```bash
claude mcp add --transport http eto https://mcp.eto.tools/mcp \
  --header "X-Eto-API-Key: eto_your_key"
```

Clients that support connectors (Claude, ChatGPT, Perplexity) can instead add the URL and sign in with Eto — an OAuth 2.1 flow with dynamic client registration and PKCE, after which calls carry a bearer token.

## Endpoint index

- **Listing Builder** (6): `listing-schema`, `listing-create`, `listing-create-status`, `store-details-cached`, `store-sync`, `file-upload`
- **Search** (1): `listings-search`
- **Listings** (11): `listing-detail`, `listing-delete`, `listing-images`, `listing-reviews`, `listing-inventory-get`, `listing-inventory-update`, `listing-videos`, `listing-personalization`, `listings-batch`, `listing-offering`, `listing-product`
- **Shops** (8): `shop-detail`, `shop-reviews`, `shop-sections-get`, `shop-sections-create`, `shop-update`, `shop-search`, `shop-section-detail`, `section-listings`
- **Store Management** (5): `shop-listings`, `shop-listing-update`, `shop-active-listings`, `shop-featured-listings`, `listing-translation`
- **Finance** (3): `finance`, `finance-sync`, `finance-software-expenses`
- **Images & Media** (9): `listing-image-upload`, `listing-image-delete`, `listing-video-upload`, `listing-video-delete`, `listing-variation-images`, `listing-file-upload`, `listing-file-detail`, `listing-image-detail`, `listing-video-detail`
- **Listing Properties** (4): `listing-property`, `listing-property-public`, `listing-properties-all`, `listing-property-delete`
- **Shipping** (7): `shipping-profiles`, `shipping-profile-detail`, `shipping-destinations`, `shipping-destination-detail`, `shipping-upgrades`, `shipping-upgrade-detail`, `shipping-carriers`
- **Categories** (4): `categories`, `category-properties`, `buyer-categories`, `buyer-category-properties`
- **User** (3): `users-me`, `user-shops`, `user-profile`
- **Shop Policies** (14): `shop-return-policies`, `shop-production-partners`, `shop-listing-requirements`, `return-policy-create`, `return-policy-detail`, `return-policy-consolidate`, `return-policy-listings`, `shop-holidays`, `shop-holiday-update`, `listing-requirement-detail`, `shipping-profiles-live`, `return-policies-live`, `processing-profiles-live`, `shop-sections-live`
- **Orders** (9): `shop-orders`, `shop-order-detail`, `shop-order-tracking`, `order-update`, `order-listings`, `order-payments`, `orders-list`, `order-detail`, `orders-sync`
- **Finances** (8): `shop-finance-transactions`, `shop-finance-payments`, `shop-finance-sales`, `listing-sales`, `order-transactions`, `single-transaction`, `single-ledger-entry`, `shop-all-payments`
- **Analytics** (1): `shop-traffic-stats`
- **Stores** (2): `stores-list`, `store-detail`
- **Research** (1): `product-hunt`
- **AI Studio** (6): `ai-chat`, `ai-generate-title`, `ai-generate-description`, `ai-generate-image`, `ai-generate-image-status`, `ai-analyze-image`
- **Webhooks** (2): `webhooks-deliveries`, `webhook-delivery-retry`

## Listing Builder

Create a complete Etsy listing from one JSON payload, and fetch the category schema and store profiles it needs.

### Listing Builder guide

#### How It Works

The ETO Listing Builder lets you create Etsy listings with a single API call. ETO handles all the complexity — variation property ordering, Cartesian product completion, price/quantity/SKU auto-detection, image uploading, category attributes, and personalization.

Three modes:

| | |
| --- | --- |
| `state="draft"` | Saves to ETO only. Zero Etsy calls. Instant. You can review and edit in the ETO dashboard before publishing. |
| `state="publish"` | Saves to ETO + publishes to Etsy as a draft listing. Synchronous: the request blocks for roughly 5–15 seconds per shop and returns the final results (including Etsy image URLs) in the response body. |
| `state="active"` | Same as publish + activates 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 see your connected shops.

#### Step 1: Get Your Store Data

Before creating a listing, you need IDs for shipping profiles, return policies, processing profiles, and shop sections. We give you four dedicated "live" endpoints — one per profile type. Each one is a single GET, makes ONE Etsy API call, returns the absolute latest data right now, never from cache.

Live profile endpoints (always fresh, no cache):

| Endpoint | Gives you |
| --- | --- |
| `GET /api/v1/stores/{shop_id}/shipping-profiles/live` | shipping_profile_id |
| `GET /api/v1/stores/{shop_id}/return-policies/live` | return_policy_id |
| `GET /api/v1/stores/{shop_id}/processing-profiles/live` | processing_profile_id (a.k.a. readiness_state_id) |
| `GET /api/v1/stores/{shop_id}/shop-sections/live` | shop_section_id |

Each response is the same shape:

```json
{
  "shop_id": "12345678",
  "fetched_at": "2026-04-19T14:30:00Z",
  "count": 2,
  "<profile_type>": [ { "<id_field>": ..., "title": "...", ... } ]
}
```

Example — fetch all four in parallel from your client:

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/shipping-profiles/live"
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/return-policies/live"
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/processing-profiles/live"
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/shop-sections/live"
```

IDs you will need when building the listing payload:

| | |
| --- | --- |
| `shipping_profile_id` | Required for physical listings. |
| `return_policy_id` | Required for physical listings. |
| `processing_profile_id` | Controls how the processing time is shown. |
| `shop_section_id` | Optional. Files the listing under a shop section. |

Why use these instead of /details or /sync?

| | |
| --- | --- |
| `/details` | Serves cached data. It can be stale, and it omits return_policies and processing_profiles. |
| `/sync` | Refreshes everything in one shot, but it is heavier (about 5 Etsy calls). Use it only when you want the full snapshot refreshed. |
| `/live` | The simplest path: pick the profile type you need, get the latest data, done. |

#### Step 2: Get the Category Schema

Each Etsy category has different attributes. Call the schema endpoint to discover them:

```http
GET /api/v1/categories/{category_id}/listing-schema
```

This returns:

```text
- required_fields: title, description, price, quantity, taxonomy_id, listing_type
- optional_fields: tags, materials, styles, sku, who_made, when_made, dimensions, weight, etc.
- category_attributes: dynamic attributes for this category (e.g. Primary color, Sleeve length, Neckline)
  Each attribute has: property_id, name, required, possible_values (with value_id and name), scales
- variation_properties: which properties can be used for product variations (e.g. Color, Size)
```

Cache this response — categories rarely change.

Example: Category 482 (T-Shirts) has 11 attributes:

```text
Materials, Primary color, Secondary color, Size, Sustainability, Sleeve length,
Neckline, Clothing style, Occasion, Holiday, Graphic
```

#### Step 3: Upload Images (Two Options)

Option A — Remote URLs:

```text
Pass publicly accessible URLs directly in images[].url. ETO downloads them server-side.
Example: "url": "https://i.etsystatic.com/12345/image.jpg"
```

Option B — Local files:

```text
1. Upload your file:  POST /api/v1/uploads  (multipart/form-data with "file" field)
2. Use the returned eto-upload:// URL in images[].url
```

```text
Example:
  curl -X POST -H "X-Eto-API-Key: eto_..." -F "file=@my_photo.jpg" -F "type=image" https://eto.tools/api/v1/uploads
  Response: {"url": "eto-upload:///path/to/file.jpg", ...}
  Then use: "images": [{"url": "eto-upload:///path/to/file.jpg", "rank": 1}]
```

Images:

```text
- Min 1, max 20 per listing
- Formats: JPG, PNG, GIF (max 100MB each)
- rank: 1-20 (rank 1 = primary photo shown to buyers)
- For variation images: use the index (0-based) in variation_images.mapping
```

#### Step 4: Create the Listing

Send a single POST with everything:

```http
POST /api/v1/listings/create
```

The request body is a JSON object with these top-level keys:

```text
state, shops[], listing{}, images[], personalization{}, category_attributes{}, variations{}
Optional: digital_files[], videos[]
```

For state="draft": Returns instantly with listing_pk and dashboard_url. For state="publish"/"active": Returns job_id. Poll with GET /api/v1/listings/create/{job_id}.

#### Variations — How They Work

Etsy allows up to 2 variation properties per listing (e.g. Color + Size). You provide the data — ETO automatically detects what varies and sets the correct Etsy toggles:

```text
- Prices vary: if offerings have different prices, ETO detects which property causes it
- Quantities vary: same auto-detection
- SKUs vary: if each offering has a unique sku, ETO sets sku_on_property
- Processing profiles vary: if offerings have different processing_profile_id values
- Photos vary: if you provide variation_images.mapping, ETO links images to property values
```

Structure:

```json
"variations": {
  "properties": [
    {"property_id": 200, "name": "Color", "values": ["Black", "White", "Navy"]},
    {"property_id": 62809790533, "name": "Size", "scale_id": 17, "values": ["S", "M", "L", "XL"]}
  ],
  "offerings": [
    {"color": "Black", "size": "S", "price": 29.99, "quantity": 20, "sku": "BLK-S", "processing_profile_id": 123},
    {"color": "Black", "size": "XL", "price": 34.99, "quantity": 15, "sku": "BLK-XL", "processing_profile_id": 456},
    ...
  ],
  "variation_images": {
    "property": "color",
    "mapping": {"Black": 0, "White": 1, "Navy": 2}
  }
}
```

Key rules:

```text
- Offering keys use the lowercased property name (e.g. "color", "size")
- Each offering value must match one of the property's values array
- variation_images.mapping values are 0-based indices into the images[] array
- Only ONE property can have variation images (Etsy constraint)
- ETO fills in missing combinations with is_enabled=false automatically
```

#### Category Attributes

Each Etsy category has specific attributes (e.g. T-Shirts have Primary color, Sleeve length, Neckline). Use the listing-schema endpoint to discover them, then pass the values you want:

```json
"category_attributes": {
  "200": {"value_ids": [1], "values": ["Black"]},
  "325502675244": {"value_ids": [2668], "values": ["Short sleeve"]},
  "325502675262": {"value_ids": [2678], "values": ["Crew"]}
}
```

The key is the property_id (from listing-schema category_attributes). The value_ids and values come from listing-schema possible_values. Some properties have scales (like Size) — include scale_id when applicable.

#### Personalization

Etsy's multi-question personalization system lets sellers collect up to 5 custom details from buyers. Pass a "questions" array inside the personalization object. ETO handles the Etsy API translation.

Limits:

```text
- Maximum 5 questions per listing
- Maximum 1 file-upload question (unlabeled_upload or labeled_upload) per listing
- Set "enabled": false (or omit personalization entirely) to disable
```

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ QUESTION TYPES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. text_input — Free text field

```text
Buyers type freeform text (names, dates, quotes, etc.).
```

```text
Fields:
  question_text          string   REQUIRED  1-45 chars, must start with letter/number
  instructions           string   optional  max 120 chars, must start with letter/number
  required               boolean  optional  default false
  max_allowed_characters  integer  optional  1-1024, default 256
```

```text
Example:
  {
    "question_type": "text_input",
    "question_text": "Name for engraving",
    "instructions": "Enter first and last name exactly as you want it engraved",
    "required": true,
    "max_allowed_characters": 30
  }
```

2. dropdown — List of options

```text
Buyers select from a predefined list. No instructions field allowed.
```

```text
Fields:
  question_text  string   REQUIRED  1-45 chars, must start with letter/number
  required       boolean  optional  default false
  options        array    REQUIRED  1-30 items
    options[].label  string  REQUIRED  1-20 chars, must be unique (case-insensitive)
```

```text
DO NOT include "instructions" — Etsy rejects dropdowns with instructions.
```

```text
Example:
  {
    "question_type": "dropdown",
    "question_text": "Thread color",
    "required": true,
    "options": [
      {"label": "Gold"},
      {"label": "Silver"},
      {"label": "Rose Gold"},
      {"label": "Black"}
    ]
  }
```

3. unlabeled_upload — File upload

```text
Buyers upload files (images, PDFs, etc.) without per-file labels.
Accepted formats: .jpg, .png, .svg, .pdf, .heic (up to 100MB each).
```

```text
Fields:
  question_text     string   REQUIRED  1-45 chars, must start with letter/number
  instructions      string   optional  max 120 chars, must start with letter/number
  required          boolean  optional  default false
  max_allowed_files  integer  optional  1-10, default 1
```

```text
Example:
  {
    "question_type": "unlabeled_upload",
    "question_text": "Upload your logo",
    "instructions": "High-resolution PNG or SVG preferred, minimum 300 DPI",
    "required": true,
    "max_allowed_files": 3
  }
```

4. labeled_upload — File upload with labels

```text
Like unlabeled_upload, but each file slot has a named label shown to the buyer.
The options array MUST have exactly as many items as max_allowed_files.
```

```text
Fields:
  question_text     string   REQUIRED  1-45 chars, must start with letter/number
  instructions      string   optional  max 120 chars, must start with letter/number
  required          boolean  optional  default false
  max_allowed_files  integer  REQUIRED  2-10 (minimum 2 for labeled uploads)
  options           array    REQUIRED  exactly max_allowed_files items
    options[].label  string  REQUIRED  1-45 chars
```

```text
Example:
  {
    "question_type": "labeled_upload",
    "question_text": "Upload design files",
    "instructions": "Please upload high-res files only",
    "required": true,
    "max_allowed_files": 3,
    "options": [
      {"label": "Front design"},
      {"label": "Back design"},
      {"label": "Sleeve design"}
    ]
  }
```

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ FULL EXAMPLE — All 4 question types ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

```json
"personalization": {
  "enabled": true,
  "questions": [
    {
      "question_type": "text_input",
      "question_text": "Name for engraving",
      "instructions": "Enter first and last name",
      "required": true,
      "max_allowed_characters": 30
    },
    {
      "question_type": "text_input",
      "question_text": "Special date",
      "instructions": "Format: MM/DD/YYYY",
      "required": false,
      "max_allowed_characters": 10
    },
    {
      "question_type": "dropdown",
      "question_text": "Font style",
      "required": true,
      "options": [
        {"label": "Script"},
        {"label": "Block"},
        {"label": "Serif"},
        {"label": "Handwritten"}
      ]
    },
    {
      "question_type": "dropdown",
      "question_text": "Gift wrapping",
      "required": false,
      "options": [
        {"label": "None"},
        {"label": "Standard box"},
        {"label": "Premium box"}
      ]
    },
    {
      "question_type": "labeled_upload",
      "question_text": "Upload design files",
      "instructions": "High-res PNG or SVG, minimum 300 DPI",
      "required": true,
      "max_allowed_files": 2,
      "options": [
        {"label": "Front design"},
        {"label": "Back design"}
      ]
    }
  ]
}
```

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ VALIDATION ERRORS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If validation fails, ETO returns 400 with specific field-level errors:

```json
{"error": "VALIDATION_FAILED", "fields": {
  "personalization.questions": "Maximum 5 questions allowed.",
  "personalization.questions.upload": "Maximum 1 upload-type question.",
  "personalization.questions[0].question_text": "Required.",
  "personalization.questions[0].question_text": "Max 45 characters. Provided: 52.",
  "personalization.questions[0].question_text": "Must start with a letter or number.",
  "personalization.questions[0].instructions": "Dropdown questions cannot have instructions.",
  "personalization.questions[0].instructions": "Max 120 characters. Provided: 135.",
  "personalization.questions[0].max_allowed_characters": "Must be 1-1024. Provided: 2000.",
  "personalization.questions[0].max_allowed_files": "Must be 2-10. Provided: 1.",
  "personalization.questions[0].options": "At least 1 option required.",
  "personalization.questions[0].options[2].label": "Option label cannot be empty.",
  "personalization.questions[0].options[2].label": "Max 20 characters. Provided: 25.",
  "personalization.questions[0].options[2].label": "Duplicate option: \"Gold\".",
  "personalization.questions[0].options": "Labeled upload requires exactly 3 file labels (matching max_allowed_files). Provided: 2."
}}
```

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DISABLING PERSONALIZATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

To remove all personalization from a listing:

```json
"personalization": {"enabled": false}
```

Or simply omit the personalization object entirely. On existing listings, this calls DELETE on the Etsy personalization endpoint.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ LEGACY SHORTHAND (still supported) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If you omit "questions" but set "enabled": true, ETO creates a single text_input question from the flat fields. This is for backward compatibility only — use "questions" for new code.

```json
"personalization": {
  "enabled": true,
  "is_required": false,
  "instructions": "Enter name or initials",
  "max_chars": 15
}
```

Equivalent to:

```json
"personalization": {
  "enabled": true,
  "questions": [{
    "question_type": "text_input",
    "question_text": "Personalization",
    "instructions": "Enter name or initials",
    "required": false,
    "max_allowed_characters": 15
  }]
}
```

#### Complete Example — Maxed Out Physical Listing

This example creates a physical listing with EVERY feature: 5 images, 12 variations (3 colors x 4 sizes), prices/quantities/SKUs/processing profiles all varying, photos linked to colors, 5 personalization questions (2 text + 2 dropdown + 1 labeled upload), all 11 category attributes, full store settings.

```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": "publish",
  "shops": [{
    "shop_id": 57124360,
    "shipping_profile_id": 291257702631,
    "return_policy_id": 1435074483195,
    "processing_profile_id": 1456101932490,
    "shop_section_id": 56909368
  }],
  "listing": {
    "title": "Vintage Band Tee - Custom Graphic Design",
    "description": "Premium cotton vintage-style t-shirt...",
    "listing_type": "physical",
    "taxonomy_id": 482,
    "price": 29.99,
    "quantity": 100,
    "who_made": "i_did",
    "when_made": "2020_2026",
    "sku": "VBT-MAIN",
    "tags": ["vintage tee", "band shirt", "graphic tee", ...],
    "materials": ["cotton", "polyester"],
    "styles": ["Casual", "Vintage"],
    "item_weight": 6.0,
    "item_weight_unit": "oz"
  },
  "images": [
    {"url": "https://example.com/black-tee.jpg", "rank": 1},
    {"url": "https://example.com/white-tee.jpg", "rank": 2},
    {"url": "https://example.com/navy-tee.jpg", "rank": 3},
    {"url": "https://example.com/detail-shot.jpg", "rank": 4},
    {"url": "https://example.com/size-chart.jpg", "rank": 5}
  ],
  "personalization": {
    "enabled": true,
    "questions": [
      {
        "question_type": "text_input",
        "question_text": "Name for print",
        "instructions": "First and last name as you want it printed",
        "required": true,
        "max_allowed_characters": 30
      },
      {
        "question_type": "text_input",
        "question_text": "Special date",
        "instructions": "Format: MM/DD/YYYY",
        "required": false,
        "max_allowed_characters": 10
      },
      {
        "question_type": "dropdown",
        "question_text": "Font style",
        "required": true,
        "options": [
          {"label": "Script"},
          {"label": "Block"},
          {"label": "Serif"},
          {"label": "Handwritten"}
        ]
      },
      {
        "question_type": "dropdown",
        "question_text": "Gift wrapping",
        "required": false,
        "options": [
          {"label": "None"},
          {"label": "Standard box"},
          {"label": "Premium box"}
        ]
      },
      {
        "question_type": "labeled_upload",
        "question_text": "Upload design files",
        "instructions": "High-res PNG or SVG, minimum 300 DPI",
        "required": true,
        "max_allowed_files": 2,
        "options": [
          {"label": "Front design"},
          {"label": "Back design"}
        ]
      }
    ]
  },
  "category_attributes": {
    "200": {"value_ids": [1], "values": ["Black"]},
    "52047899002": {"value_ids": [10], "values": ["White"]},
    "62809790533": {"value_ids": [2139], "values": ["M"], "scale_id": 17},
    "325502675244": {"value_ids": [2668], "values": ["Short sleeve"]},
    "325502675262": {"value_ids": [2678], "values": ["Crew"]},
    "325502673988": {"value_ids": [2556], "values": ["Streetwear"]},
    "46803063641": {"value_ids": [19], "values": ["Birthday"]},
    "46803063659": {"value_ids": [35], "values": ["Christmas"]},
    "332797777099": {"value_ids": [2558], "values": ["Animal"]}
  },
  "variations": {
    "properties": [
      {"property_id": 200, "name": "Color", "values": ["Black", "White", "Navy"]},
      {"property_id": 62809790533, "name": "Size", "scale_id": 17, "values": ["S", "M", "L", "XL"]}
    ],
    "offerings": [
      {"color": "Black", "size": "S", "price": 29.99, "quantity": 20, "sku": "BLK-S", "processing_profile_id": 1456101932490},
      {"color": "Black", "size": "XL", "price": 34.99, "quantity": 15, "sku": "BLK-XL", "processing_profile_id": 1458328250863},
      {"color": "White", "size": "S", "price": 29.99, "quantity": 20, "sku": "WHT-S", "processing_profile_id": 1456101932490},
      ...
    ],
    "variation_images": {
      "property": "color",
      "mapping": {"Black": 0, "White": 1, "Navy": 2}
    }
  }
}'
```

#### Response — Draft (state="draft")

```json
{
  "listing_pk": 1135,
  "status": "draft",
  "message": "Listing saved as draft in ETO. Open it in the ETO dashboard to review and publish.",
  "dashboard_url": "/dashboard/single-research/1135/"
}
```

The listing is saved in ETO with all fields pre-filled. Open the dashboard URL to review, edit anything, and publish when ready. Zero Etsy API calls were made.

#### Response — Publish/Active (state="publish" or "active")

The request runs synchronously: it blocks for ~5–15 seconds per shop while ETO creates the draft on Etsy, uploads images/videos/files, sets inventory and variations, and fetches the final image URLs back. Set your HTTP client timeout to at least 60s (120s for multi-shop calls).

200 OK — successful publish:

```json
{
  "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}
    ],
    "videos": []
  }]
}
```

For state="active", successful shops also carry "activated": true and "activation_cost_usd": 0.20.

Per-shop failures are reported inside results[] with status="error" and an Etsy-side error payload. Other shops in the same request can still succeed.

Backwards compatibility: job_id and poll_url are still in the response so existing clients that call GET /api/v1/listings/create/{job_id} continue to work. The job will be in "completed" state on the first poll.

500 Internal Server Error — job-level failure (e.g. the entire publish flow threw):

```json
{
  "job_id": "lcj_...",
  "listing_pk": 1174,
  "status": "failed",
  "error": "<human-readable reason>",
  "poll_url": "/api/v1/listings/create/lcj_...",
  "results": []
}
```

#### Validation & Preflight Errors (400)

The listing-create endpoint validates heavily before writing any state. If a preflight check fails, you get a 400 with a fields object pointing at the exact problem:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "status": 400,
    "message": "Request validation failed.",
    "fields": {
      "images[1].url": "Uploaded file is no longer available on the server. Re-upload via POST /api/v1/uploads and use the new url.",
      "shops[0].shop_section_id": "Section 999999999 not found in shop 53081804."
    }
  }
}
```

Common preflight errors:

• images[N].url / digital_files[N].url / videos[N].url — the eto-upload:// URL you referenced no longer resolves to a file on the server. This can happen if the upload was done a long time ago or if the server was redeployed. Re-upload via POST /api/v1/uploads and use the fresh URL.

• shops[N].shop_section_id — the section ID does not belong to that shop. Call GET /api/v1/stores/{shop_id}/shop-sections/live to fetch the current sections.

• Standard field-shape errors (title too long, missing taxonomy_id, etc.) from the schema validator.

All preflight errors are fatal: if a 400 is returned, nothing was persisted on the ETO side and no Etsy draft was created. Retrying with a fixed payload is safe.

#### For AI Agents

1. Always call GET /api/v1/stores first to get valid shop_ids. 2. Get profile IDs from the four live endpoints (each returns the absolute latest, no cache):

```http
  GET /api/v1/stores/{shop_id}/shipping-profiles/live    → shipping_profile_id
  GET /api/v1/stores/{shop_id}/return-policies/live      → return_policy_id
  GET /api/v1/stores/{shop_id}/processing-profiles/live  → processing_profile_id
  GET /api/v1/stores/{shop_id}/shop-sections/live        → shop_section_id
Fetch them in parallel for speed.
```

3. Call GET /api/v1/categories/{id}/listing-schema for any new category to discover attributes and variation properties. 4. Use the "required" field on category_attributes to know which are mandatory. 5. Use "possible_values" from the schema to select valid value_ids for each attribute. 6. Default to state="draft" unless the user explicitly asks to publish or activate. 7. state="active" costs $0.20 USD per listing per shop — always warn the user. 8. For variations: just provide the data (prices, quantities, SKUs, processing_profile_id per offering). ETO auto-detects what varies. 9. For variation images: set variation_images.property to the property name (e.g. "color") and mapping to {value: image_index}. 10. Images can be URLs or local files uploaded via POST /api/v1/uploads.

### GET /api/v1/categories/{category_id}/listing-schema

Get category listing schema

- Operation ID: `listing_schema`
- Slug: `listing-schema`
- 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-schema
- Markdown: https://eto.tools/dev/docs/listing-builder/listing-schema.md

Returns the full schema for creating a listing in a specific Etsy category. Includes required fields, category-specific attributes with valid values, variation properties, and constraints. Cache this response — categories change rarely.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `category_id` | path | `integer` | yes | Etsy taxonomy/category ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/categories/2078/listing-schema"
```

```python
import requests

url = "https://eto.tools/api/v1/categories/2078/listing-schema"
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/categories/2078/listing-schema";

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

```json
{"category_id": 2078, "required_fields": {...}, "category_attributes": [...], "variation_properties": [...]}
```

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


### GET /api/v1/listings/create/{job_id}

Poll listing creation job status

- Operation ID: `listing_create_status`
- Slug: `listing-create-status`
- 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-status
- Markdown: https://eto.tools/dev/docs/listing-builder/listing-create-status.md

Returns the status and results of a listing-create job. Since publish/active now execute synchronously on the web, jobs are normally in "completed" state by the time any poll lands. This endpoint remains available for backwards compatibility and for clients that prefer the job-polling pattern. Returns per-shop results when the job completes, including listing IDs, Etsy URLs, and any per-shop warnings.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `job_id` | path | `string` | yes | The job_id returned by POST /api/v1/listings/create |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/listings/create/lcj_abc123def456"
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/create/lcj_abc123def456";

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

```json
{
  "job_id": "lcj_aDzKwnFW5sYHewZaPTduRw",
  "status": "completed",
  "created_at": "2026-04-16T21:07:20.963219+00:00",
  "completed_at": "2026-04-16T21:07:28.245564+00:00",
  "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}
      ],
      "videos": [],
      "warnings": [
        {"code": "recovery_update_failed", "fields": ["shop_section_id"], "reason": "Etsy rejected the section update"}
      ]
    }
  ]
}

# Terminal statuses: "completed" | "failed"
# On job-level failure: response also includes "error_message": "<reason>"
```

#### 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/stores/{shop_id}/details

Get cached store details (zero Etsy calls)

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

Returns all cached data for a connected store: shop details, shipping profiles, shop sections. No Etsy API calls are made. Return policies, processing profiles, and production partners return null (not cached locally). Use POST /sync to fetch those.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/details"
```

```python
import requests

url = "https://eto.tools/api/v1/stores/12345678/details"
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/stores/12345678/details";

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

```json
{
  "shop_id": "12345678",
  "shop_name": "MyShop",
  "currency_code": "USD",
  "shipping_from_country_iso": "US",
  "listing_active_count": 142,
  "review_average": 4.8,
  "review_count": 523,
  "is_vacation": false,
  "url": "https://www.etsy.com/shop/MyShop",
  "last_synced_at": "2026-04-10T14:30:00Z",
  "shipping_profiles": [
    {"shipping_profile_id": 111222, "title": "Standard Shipping", "min_processing_days": 1, "max_processing_days": 3, "primary_cost": "5.99", "currency_code": "USD"}
  ],
  "shop_sections": [
    {"shop_section_id": 44556, "title": "New Arrivals", "rank": 1, "active_listing_count": 15}
  ],
  "return_policies": null,
  "processing_profiles": null,
  "production_partners": null
}
```

#### 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/stores/{shop_id}/sync

Sync store data from Etsy

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

Triggers a fresh pull from Etsy API for all store data (details, shipping profiles, sections, return policies, processing profiles, production partners). Caches what can be cached and returns the complete updated dataset. Costs ~5 Etsy API calls.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/sync"
```

```python
import requests

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

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

```javascript
const url = "https://eto.tools/api/v1/stores/12345678/sync";

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

#### Response

```json
{
  "shop_id": "12345678",
  "shop_name": "MyShop",
  "currency_code": "USD",
  "last_synced_at": "2026-04-12T16:45:00Z",
  "shipping_profiles": [
    {"shipping_profile_id": 111222, "title": "Standard Shipping", "min_processing_days": 1, "max_processing_days": 3, "primary_cost": "5.99", "currency_code": "USD"}
  ],
  "shop_sections": [
    {"shop_section_id": 44556, "title": "New Arrivals", "rank": 1, "active_listing_count": 15}
  ],
  "return_policies": [
    {"return_policy_id": 333444, "accepts_returns": true, "accepts_exchanges": true, "return_deadline": 30}
  ],
  "processing_profiles": [
    {"readiness_state_id": 555666, "readiness_state": "ready_to_ship", "min_processing_days": 1, "max_processing_days": 3, "processing_days_display_label": "1-3 business days"}
  ],
  "production_partners": [
    {"production_partner_id": 999, "partner_name": "PrintCo", "location": "USA"}
  ]
}
```

#### 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/uploads

Upload a file (image, video, or digital file)

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

Upload a local file to ETO for use in listing creation. Returns an eto-upload:// URL that you can use in images[].url, digital_files[].url, or videos[].url when calling POST /api/v1/listings/create. This is the way to use local files instead of remote URLs.

Send as multipart/form-data with a "file" field and optional "type" field.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `file` | body | `file` | yes | The file to upload. Send as multipart/form-data. Constraints: max 100MB. |
| `type` | body | `string` | no | The type of file being uploaded. Default: `image`. One of: `image`, `video`, `digital`. |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" \
  -F "file=@my_product_photo.jpg" \
  -F "type=image" \
  "https://eto.tools/api/v1/uploads"
```

```python
import requests

url = "https://eto.tools/api/v1/uploads"
headers = {"X-Eto-API-Key": "eto_your_key"}
files = {"file": open("my_product_photo.jpg", "rb")}
data = {"type": "image"}

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

```javascript
const url = "https://eto.tools/api/v1/uploads";
const form = new FormData();
form.append("file", fileInput.files[0]);
form.append("type", "image");

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

#### Response

```json
{
  "url": "eto-upload:///path/to/uploaded/file.jpg",
  "filename": "my_product_photo.jpg",
  "size": 102400,
  "content_type": "image/jpeg",
  "type": "image",
  "note": "Use this url value in images[].url when calling POST /api/v1/listings/create."
}
```

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


## Search

Search active Etsy listings across the marketplace.

### GET /api/v1/listings/search

Search active Etsy listings

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

Search for active listings on Etsy by keyword. Returns paginated results with listing details.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `keywords` | query | `string` | yes | Search keywords |
| `limit` | query | `integer` | no | Number of results (max 100) Default: `25`. |
| `offset` | query | `integer` | no | Pagination offset Default: `0`. |
| `sort_on` | query | `string` | no | Sort field: created, price, score Default: `created`. |
| `sort_order` | query | `string` | no | Sort order: asc, desc Default: `desc`. |
| `min_price` | query | `number` | no | Minimum price filter |
| `max_price` | query | `number` | no | Maximum price filter |
| `category_id` | query | `integer` | no | Filter by product category ID (get IDs from /categories) |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/listings/search?keywords=leather+wallet&limit=10"
```

```python
import requests

url = "https://eto.tools/api/v1/listings/search?keywords=leather+wallet&limit=10"
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/search?keywords=leather+wallet&limit=10";

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

```json
{"count": 1000, "results": [{"listing_id": 123, "title": "...", "price": {"amount": 2999, "divisor": 100, "currency_code": "USD"}, ...}]}
```

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


## Listings

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

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

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 only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-inventory-get
- Markdown: https://eto.tools/dev/docs/listings/listing-inventory-get.md

Retrieve inventory data (stock levels, variations, pricing) for a listing.

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 |

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


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

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 |

#### 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 /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 · POST · DELETE /api/v1/listings/{listing_id}/personalization

Manage listing personalization

- Operation ID: `listing_personalization_get`
- Slug: `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-listing-personalization
- Markdown: https://eto.tools/dev/docs/listings/listing-personalization.md
- Operation IDs by method: `listing_personalization_get` = GET, `listing_personalization_post` = POST, `listing_personalization_delete` = DELETE

Get, create, or delete personalization questions for a listing.
GET returns {personalization_questions: [...]} with up to 5 questions.
POST replaces all questions. DELETE removes personalization entirely.
Question types: text_input, dropdown, unlabeled_upload, labeled_upload.
Max 1 file-upload question per listing. See the Listing Builder guide for full field constraints.

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 — GET

```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();
```

#### Request — POST

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

```python
import requests

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

response = requests.post(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: "POST",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
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/listings/1234567890/personalization" \
  -H "X-Eto-API-Key: eto_your_key"
```

```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/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 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-listings-batch
- Markdown: https://eto.tools/dev/docs/listings/listings-batch.md

Retrieve multiple listings in a single request.

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 |
| `includes` | query | `string` | no | Comma-separated associations: Images, Shop, User, Inventory |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/listings/batch?listing_ids=123,456,789"
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";

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.

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 only. Reads public marketplace data, so no store needs to be connected.
- 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.


## Shops

Public shop lookups: details, sections, reviews, production partners.

### GET /api/v1/shops/{shop_id}

Get shop details

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

Retrieve detailed information about a shop including name, description, ratings, and listing counts.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | The Etsy shop ID |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/shops/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/shops/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/shops/{shop_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/shops/{shop_id}/reviews

Get shop reviews

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

Retrieve reviews for a specific shop.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | The Etsy shop 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/shops/12345678/reviews?limit=5"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/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/shops/12345678/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/shops/{shop_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/shops/{shop_id}/sections

Get shop sections

- Operation ID: `shop_sections_get`
- Slug: `shop-sections-get`
- Category: Shops
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-shop-sections-get
- Markdown: https://eto.tools/dev/docs/shops/shop-sections-get.md

Retrieve product sections for a shop.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | The Etsy shop ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/sections"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/sections"
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/sections";

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/shops/{shop_id}/sections` 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/shops/{shop_id}/sections

Create shop section

- Operation ID: `shop_sections_create`
- Slug: `shop-sections-create`
- Category: Shops
- 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-sections-create
- Markdown: https://eto.tools/dev/docs/shops/shop-sections-create.md

Create a new product section in your shop.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `title` | body | `string` | yes | Section title |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"title": "New Section"}' "https://eto.tools/api/v1/shops/12345678/sections"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/sections"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "title": "New Section"
}

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/sections";
const payload = {
  "title": "New Section"
};

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}/sections` 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/shops/{shop_id}

Update shop details

- Operation ID: `shop_update`
- Slug: `shop-update`
- Category: Shops
- 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-update
- Markdown: https://eto.tools/dev/docs/shops/shop-update.md

Update your shop's title, announcement, or other settings. Etsy docs: "Update Shop".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -X PUT -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"title": "My Shop Name"}' "https://eto.tools/api/v1/shops/12345678"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "title": "My Shop Name"
}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678";
const payload = {
  "title": "My Shop Name"
};

const response = await fetch(url, {
  method: "PUT",
  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}` 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/shops

Search shops

- Operation ID: `shop_search`
- Slug: `shop-search`
- Category: Shops
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-shop-search
- Markdown: https://eto.tools/dev/docs/shops/shop-search.md

Search for Etsy shops by name. Etsy docs: "Find Shops".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_name` | query | `string` | yes | Shop name to search for |
| `limit` | query | `integer` | no | Number of results (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/shops?shop_name=handmade&limit=5"
```

```python
import requests

url = "https://eto.tools/api/v1/shops?shop_name=handmade&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/shops?shop_name=handmade&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/shops` 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 · PUT · DELETE /api/v1/shops/{shop_id}/sections/{section_id}

Get, update, or delete a section

- Operation ID: `shop_section_detail_get`
- Slug: `shop-section-detail`
- Category: Shops
- 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-section-detail
- Markdown: https://eto.tools/dev/docs/shops/shop-section-detail.md
- Operation IDs by method: `shop_section_detail_get` = GET, `shop_section_detail_put` = PUT, `shop_section_detail_delete` = DELETE

Manage a specific shop section. Etsy docs: "Shop Section".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `section_id` | path | `integer` | yes | The section ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/sections/12345"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/sections/12345"
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/sections/12345";

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 "https://eto.tools/api/v1/shops/12345678/sections/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

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

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/sections/1";

const response = await fetch(url, {
  method: "PUT",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
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/sections/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/sections/1"
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/sections/1";

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}/sections/{section_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/shops/{shop_id}/section-listings

Get listings by section

- Operation ID: `section_listings`
- Slug: `section-listings`
- Category: Shops
- 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-section-listings
- Markdown: https://eto.tools/dev/docs/shops/section-listings.md

Get all listings organized by shop section. Etsy docs: "Listings By Section".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/section-listings"
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/section-listings";

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/shops/{shop_id}/shop-sections/listings` 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.


## Store Management

Create and edit listings inside a shop you own.

### GET · POST /api/v1/shops/{shop_id}/listings

List or create shop listings

- Operation ID: `shop_listings_get`
- Slug: `shop-listings`
- Category: Store Management
- 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-listings
- Markdown: https://eto.tools/dev/docs/store-management/shop-listings.md
- Operation IDs by method: `shop_listings_get` = GET, `shop_listings_post` = POST

GET: Retrieve your shop listings. POST: Create a new draft listing.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `limit` | query | `integer` | no | Number of listings (max 100) Default: `25`. |
| `offset` | query | `integer` | no | Pagination offset Default: `0`. |
| `state` | query | `string` | no | Listing state: active, draft, inactive Default: `active`. |
| `sort_on` | query | `string` | no | Sort field Default: `created`. |
| `sort_order` | query | `string` | no | Sort direction Default: `desc`. |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/listings?limit=10&state=active"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings?limit=10&state=active"
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?limit=10&state=active";

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 — POST

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings"
headers = {"X-Eto-API-Key": "eto_your_key"}
params = {
    "limit": 25,
    "offset": 0
}

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

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

const response = await fetch(url, {
  method: "POST",
  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` 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.


### PATCH /api/v1/shops/{shop_id}/listings/{listing_id}

Update a listing

- Operation ID: `shop_listing_update`
- Slug: `shop-listing-update`
- Category: Store Management
- 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-update
- Markdown: https://eto.tools/dev/docs/store-management/shop-listing-update.md

Update properties of a listing you own.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID to update |

#### Request

```bash
curl -X PATCH -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"title": "Updated Title"}' "https://eto.tools/api/v1/shops/12345678/listings/1234567890"
```

```python
import requests

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

response = requests.patch(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";
const payload = {
  "title": "Updated Title"
};

const response = await fetch(url, {
  method: "PATCH",
  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}` 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/shops/{shop_id}/listings/active

Get active listings only

- Operation ID: `shop_active_listings`
- Slug: `shop-active-listings`
- Category: Store Management
- 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-active-listings
- Markdown: https://eto.tools/dev/docs/store-management/shop-active-listings.md

Get only active (live) listings for your shop. Etsy docs: "Active Listings By Shop".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `limit` | query | `integer` | no | Number of results (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/shops/12345678/listings/active?limit=10"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/active?limit=10"
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/active?limit=10";

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/shops/{shop_id}/listings/active` 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/shops/{shop_id}/listings/featured

Get featured listings

- Operation ID: `shop_featured_listings`
- Slug: `shop-featured-listings`
- Category: Store Management
- 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-featured-listings
- Markdown: https://eto.tools/dev/docs/store-management/shop-featured-listings.md

Get listings featured on your shop's homepage. Etsy docs: "Featured Listings By Shop".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `limit` | query | `integer` | no | Number of results (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/shops/12345678/listings/featured?limit=5"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/featured?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/shops/12345678/listings/featured?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/shops/{shop_id}/listings/featured` 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 · POST · PUT /api/v1/shops/{shop_id}/listings/{listing_id}/translations/{language}

Manage listing translations

- Operation ID: `listing_translation_get`
- Slug: `listing-translation`
- Category: Store Management
- 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-translation
- Markdown: https://eto.tools/dev/docs/store-management/listing-translation.md
- Operation IDs by method: `listing_translation_get` = GET, `listing_translation_post` = POST, `listing_translation_put` = PUT

Get, create, or update a listing translation for a specific language. Etsy docs: "Listing Translation".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `language` | path | `string` | yes | Language code (e.g. "fr", "de", "es") |

#### Request — GET

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/translations/fr"
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/translations/fr";

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 — POST

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

```python
import requests

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

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

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

const response = await fetch(url, {
  method: "POST",
  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 "https://eto.tools/api/v1/shops/12345678/listings/1234567890/translations/fr" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

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

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

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

const response = await fetch(url, {
  method: "PUT",
  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}/translations/{language}` 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.


## Finance

Eto’s own revenue, fee, refund and profit figures for a connected store.

### GET /api/v1/finance/{shop_id}

Get all finance metrics

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

Returns every financial metric for a store over a date range. Zero Etsy API calls — reads directly from the Eto database. Instant.

━━━ WHAT YOU GET BACK (always returned) ━━━

shop_id — the store
shop_name — store display name
currency — e.g. "GBP", "USD"
start / end — the range you requested
balance_cents — current store balance (latest ledger entry, not range-scoped)
paid_out_cents — total deposited to bank in this range
sales_count — number of sales
gross_sales_cents — raw sale revenue before any deductions
sales_cents — display sales value (gross - refunds - cancelled + remitted tax)
fees_cents — total Etsy fees charged (negative number)
ads_cents — total ad spend (negative number)
refund_cents — total refunded to buyers
cancelled_cents — total cancelled order amounts
recoup_cents — Etsy recovering money from your balance
remitted_tax_cents — sales tax / VAT remitted by Etsy (negative)
gross_profit_cents — profit before COGS (gross + fees + ads + tax)
cogs_cents — cost of goods sold (from your item costs in Eto)
net_profit_cents — gross profit minus COGS

All amounts in cents. Divide by 100 for display. Negative = money out.

━━━ HOW TO PICK THE DATE WINDOW ━━━

EASIEST — pass `period`: today, yesterday, last_7_days, last_30_days, this_month, last_month.
  The server resolves it in the shop timezone (override with `timezone`). No timestamps, no math.
Specific days — pass `start_date` / `end_date` as "YYYY-MM-DD" (inclusive).
Advanced — pass raw `start` / `end` unix seconds (range is [start, end), max 366 days).
The response echoes back a `range` object (exact start/end unix, start_local/end_local ISO,
timezone, label) AND the current server time — so you never compute or guess dates, and never
need a separate "what time is it" call. If the user means THEIR calendar day, pass their timezone.
Any store — use any shop_id connected to your Eto account (GET /api/v1/stores to list them).

━━━ BREAKDOWNS (optional) ━━━

Add &breakdown= to get detailed sub-breakdowns. Combine with commas.

breakdown=fees — returns breakdown.fees with:
  listing, listing_credits, transaction, transaction_credits,
  processing, processing_credits, shipping, shipping_credits,
  vat_on_fees, vat_on_fees_credits, regulatory, regulatory_credits,
  other, total

breakdown=ads — returns breakdown.ads with:
  etsy_ads, offsite_ads, offsite_ads_credits, subscription, total

breakdown=sales — returns breakdown.sales with:
  gross_sales_cents, refund_cents, cancelled_cents,
  remitted_tax: { sales_tax, sales_tax_credits, vat_ep, vat_ep_credits, total }

breakdown=gross — same as sales (alias)

breakdown=profit — returns both breakdown.gross_profit and breakdown.net_profit:
  gross_profit: { gross_sales_cents, fees_cents, ads_cents, remitted_tax_cents, total }
  net_profit: { gross_profit_cents, cogs_cents, total }

Combine: &breakdown=fees,ads,profit returns all three at once.
Omit entirely to get just the top-level numbers.

━━━ WHAT HAS BREAKDOWNS vs WHAT DOESN'T ━━━

HAS breakdown: fees, ads, sales/gross, profit
NO breakdown: balance_cents, paid_out_cents, sales_count, refund_cents,
  cancelled_cents, recoup_cents, cogs_cents, remitted_tax_cents
  (some of these appear inside the sales or profit breakdown instead)

━━━ HOURLY BREAKDOWN (optional, single-day only) ━━━

Add &hourly=true to get a per-hour breakdown alongside the day summary.
Only works when your range is 24 hours or less (single day).
If your range is longer than 24h, hourly returns null with a note.

What you get: an "hourly" array with one object per hour, from hour 0
(midnight) up to the hour containing your end time.

Each hour object contains:
  hour — 0-23 (the hour index)
  hour_start — unix timestamp of this hour's start
  hour_end — unix timestamp of this hour's end
  entry_count — number of ledger entries in this hour
  sales_count — number of sales
  gross_sales_cents — raw revenue for this hour
  sales_cents — display sales (gross - refunds - cancelled + tax)
  fees_cents — Etsy fees charged this hour
  ads_cents — ad spend this hour
  remitted_tax_cents — tax remitted this hour
  refund_cents — refunds this hour
  cancelled_cents — cancellations this hour
  recoup_cents — Etsy recoupments this hour
  gross_profit_cents — profit before COGS
  cogs_cents — cost of goods sold this hour
  net_profit_cents — profit after COGS
  paid_out_cents — payouts this hour

Combine with breakdowns: &hourly=true&breakdown=fees,ads works fine.
The hourly array gives per-hour summary metrics.
The breakdown object gives sub-category detail for the FULL day.

Turn it off: just omit &hourly (default is off). No penalty for not using it.
Multi-day range: hourly is ignored (returns null + hourly_note explaining why).

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID (from GET /api/v1/stores) |
| `period` | query | `string` | no | EASIEST — a named window the server resolves for you, no timestamps: today, yesterday, last_7_days, last_30_days, this_month, last_month. |
| `start_date` | query | `string` | no | Calendar day "YYYY-MM-DD" (inclusive). Use with end_date for a custom range. |
| `end_date` | query | `string` | no | Calendar day "YYYY-MM-DD" (inclusive). Defaults to start_date (single day). |
| `timezone` | query | `string` | no | IANA timezone for resolving period/dates (e.g. "Europe/London", "America/New_York"). Defaults to the shop's local timezone. Pass the USER's timezone (e.g. Europe/Helsinki) if they mean their own calendar day. |
| `start` | query | `integer` | no | Advanced: raw range start as unix seconds (inclusive). Prefer period/start_date. |
| `end` | query | `integer` | no | Advanced: raw range end as unix seconds (exclusive, max 366 days). Prefer period/start_date. |
| `breakdown` | query | `string` | no | Comma-separated breakdowns to include: fees, ads, sales, gross, profit. Omit for top-level numbers only. |
| `hourly` | query | `string` | no | Set to "true" to get per-hour metrics. Only works for single-day queries (range ≤ 24h). Returns array of 24 hour objects. Default: off. |

#### Request

```bash
# ── All metrics for a period (no breakdown) ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1717113600"

# ── All metrics for a single day ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1716595200"

# ── All metrics + fee breakdown ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1717113600&breakdown=fees"

# ── All metrics + ads breakdown ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1717113600&breakdown=ads"

# ── All metrics + profit breakdown (shows gross + net) ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1717113600&breakdown=profit"

# ── All metrics + sales/tax breakdown ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1717113600&breakdown=sales"

# ── All metrics + EVERY breakdown combined ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1717113600&breakdown=fees,ads,sales,profit"

# ── Single day + fee and ads breakdown ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1716595200&breakdown=fees,ads"

# ── Single day + hourly breakdown (per-hour metrics) ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1716595200&hourly=true"

# ── Single day + hourly + fee breakdown (combine both) ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1716595200&hourly=true&breakdown=fees"
```

```python
import requests

url = "https://eto.tools/api/v1/finance/12345678?start=1716508800&end=1717113600"
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/finance/12345678?start=1716508800&end=1717113600";

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

```
// ── Response WITHOUT breakdown ──
{
  "shop_id": "12345678",
  "shop_name": "My Etsy Shop",
  "currency": "GBP",
  "start": 1716508800,
  "end": 1717113600,
  "balance_cents": 125430,
  "paid_out_cents": 90000,
  "sales_count": 15,
  "gross_sales_cents": 125000,
  "sales_cents": 120000,
  "fees_cents": -15000,
  "ads_cents": -5000,
  "refund_cents": 8000,
  "cancelled_cents": 2000,
  "recoup_cents": 0,
  "remitted_tax_cents": -3000,
  "gross_profit_cents": 102000,
  "cogs_cents": 30000,
  "net_profit_cents": 72000
}

// ── Response WITH breakdown=fees,ads,sales,profit ──
{
  "shop_id": "12345678",
  "shop_name": "My Etsy Shop",
  "currency": "GBP",
  "start": 1716508800,
  "end": 1717113600,
  "balance_cents": 125430,
  "paid_out_cents": 90000,
  "sales_count": 15,
  "gross_sales_cents": 125000,
  "sales_cents": 120000,
  "fees_cents": -15000,
  "ads_cents": -5000,
  "refund_cents": 8000,
  "cancelled_cents": 2000,
  "recoup_cents": 0,
  "remitted_tax_cents": -3000,
  "gross_profit_cents": 102000,
  "cogs_cents": 30000,
  "net_profit_cents": 72000,
  "breakdown": {
    "fees": {
      "listing": -400,
      "listing_credits": 40,
      "transaction": -8125,
      "transaction_credits": 325,
      "processing": -4800,
      "processing_credits": 192,
      "shipping": -650,
      "shipping_credits": 0,
      "vat_on_fees": -1200,
      "vat_on_fees_credits": 0,
      "regulatory": -500,
      "regulatory_credits": 0,
      "other": 0,
      "total": -15118
    },
    "ads": {
      "etsy_ads": -3000,
      "offsite_ads": -1500,
      "offsite_ads_credits": 0,
      "subscription": -999,
      "total": -5499
    },
    "sales": {
      "gross_sales_cents": 125000,
      "refund_cents": 8000,
      "cancelled_cents": 2000,
      "remitted_tax": {
        "sales_tax": -2000,
        "sales_tax_credits": 200,
        "vat_ep": -1200,
        "vat_ep_credits": 0,
        "total": -3000
      }
    },
    "gross_profit": {
      "gross_sales_cents": 125000,
      "fees_cents": -15000,
      "ads_cents": -5000,
      "remitted_tax_cents": -3000,
      "total": 102000
    },
    "net_profit": {
      "gross_profit_cents": 102000,
      "cogs_cents": 30000,
      "total": 72000
    }
  }
}

// ── Response WITH hourly=true (single day) ──
{
  "shop_id": "12345678",
  "shop_name": "My Etsy Shop",
  "currency": "GBP",
  "start": 1716508800,
  "end": 1716595200,
  "balance_cents": 125430,
  "paid_out_cents": 12000,
  "sales_count": 5,
  "gross_sales_cents": 45000,
  "sales_cents": 43500,
  "fees_cents": -5400,
  "ads_cents": -1200,
  "refund_cents": 1500,
  "cancelled_cents": 0,
  "recoup_cents": 0,
  "remitted_tax_cents": 0,
  "gross_profit_cents": 38400,
  "cogs_cents": 10000,
  "net_profit_cents": 28400,
  "hourly": [
    {
      "hour": 0,
      "hour_start": 1716508800,
      "hour_end": 1716512400,
      "entry_count": 0,
      "sales_count": 0,
      "gross_sales_cents": 0,
      "sales_cents": 0,
      "fees_cents": 0,
      "ads_cents": 0,
      "remitted_tax_cents": 0,
      "refund_cents": 0,
      "cancelled_cents": 0,
      "recoup_cents": 0,
      "gross_profit_cents": 0,
      "cogs_cents": 0,
      "net_profit_cents": 0,
      "paid_out_cents": 0
    },
    { "hour": 1, "..." : "..." },
    { "hour": 2, "..." : "..." },
    "... (hours 3-8 omitted for brevity) ...",
    {
      "hour": 9,
      "hour_start": 1716541200,
      "hour_end": 1716544800,
      "entry_count": 4,
      "sales_count": 2,
      "gross_sales_cents": 18000,
      "sales_cents": 18000,
      "fees_cents": -2160,
      "ads_cents": -600,
      "remitted_tax_cents": 0,
      "refund_cents": 0,
      "cancelled_cents": 0,
      "recoup_cents": 0,
      "gross_profit_cents": 15240,
      "cogs_cents": 4000,
      "net_profit_cents": 11240,
      "paid_out_cents": 0
    },
    "... (hours 10-23) ..."
  ]
}
```

#### 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/finance/{shop_id}/sync

Re-fetch finance data from Etsy

- Operation ID: `finance_sync`
- Slug: `finance-sync`
- Category: Finance
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-finance-sync
- Markdown: https://eto.tools/dev/docs/finance/finance-sync.md

Re-fetches ledger data from the Etsy API and saves it to the Eto database. After syncing, GET /finance/{shop_id} returns the updated numbers.

━━━ MODES ━━━

EASIEST — send { "period": "today" } (or yesterday/last_7_days/last_30_days/this_month/last_month), or { "date": "2026-06-01" }, or { "start_date": "...", "end_date": "..." }. The server resolves the window (shop timezone, override with "timezone") and re-fetches it — no unix math. Single day → day mode, multi-day → range mode. The response echoes a `range` object with the exact window + server time.

1. Incremental — send empty body or {}.
   Fetches only new entries since last sync. Fast, safe to call often.
   Use for: keeping data fresh on a schedule (e.g. every 15 min).
   Returns: new_entries, total_fetched, affected_days.

2. Single day — send { "day_start": unix_ts }.
   Re-fetches ALL entries for that day. Replaces stale data.
   Optionally include day_end (defaults to day_start + 86399).
   Use for: a day that looks wrong or incomplete.
   Returns: day_start, day_end, new_entries, total_fetched.

3. Date range — send { "start": unix_ts, "end": unix_ts }.
   Deletes existing entries in the range, re-fetches from Etsy, recomputes summaries.
   Use for: full reconciliation of a period. Heaviest mode.
   Returns: start, end, deleted_stale, new_entries.

━━━ WHAT YOU GET BACK ━━━

success — true if sync completed
mode — "incremental", "day", or "range"
new_entries — number of new ledger entries saved
total_fetched — total entries fetched from Etsy (incremental + day modes)
affected_days — list of day timestamps that had changes (incremental mode only)
deleted_stale — entries deleted before re-fetch (range mode only)
day_start / day_end — the day that was synced (day mode only)
start / end — the range that was synced (range mode only)

━━━ NOTES ━━━

This is the ONLY finance endpoint that makes Etsy API calls.
Counts toward your Eto rate limit AND Etsy API cost billing.
If the store's OAuth token expired, returns STORE_TOKEN_EXPIRED.
If Etsy's API is down, returns UPSTREAM_ERROR with detail.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `period` | body | `string` | no | EASIEST — resolve a window server-side and re-fetch it: today, yesterday, last_7_days, last_30_days, this_month, last_month. No timestamps. |
| `date` | body | `string` | no | Re-fetch one calendar day "YYYY-MM-DD" (resolved in the shop/timezone). |
| `start_date` | body | `string` | no | Range mode — first day "YYYY-MM-DD" (inclusive). |
| `end_date` | body | `string` | no | Range mode — last day "YYYY-MM-DD" (inclusive). |
| `timezone` | body | `string` | no | IANA timezone for resolving period/date (default: shop timezone). |
| `day_start` | body | `integer` | no | Advanced single-day mode — unix timestamp of the day start. |
| `day_end` | body | `integer` | no | Advanced single-day mode — unix timestamp of the day end (default: day_start + 86399). |
| `start` | body | `integer` | no | Advanced range mode — start of range to re-fetch (unix seconds). |
| `end` | body | `integer` | no | Advanced range mode — end of range to re-fetch (unix seconds). |

#### Request

```bash
# ── Incremental sync (get latest entries) ──
curl -X POST -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/12345678/sync"

# ── Re-fetch a single day ──
curl -X POST -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{"day_start": 1716508800}' \
  "https://eto.tools/api/v1/finance/12345678/sync"

# ── Re-fetch a single day with explicit end ──
curl -X POST -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{"day_start": 1716508800, "day_end": 1716595199}' \
  "https://eto.tools/api/v1/finance/12345678/sync"

# ── Re-fetch an entire date range ──
curl -X POST -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{"start": 1716508800, "end": 1717113600}' \
  "https://eto.tools/api/v1/finance/12345678/sync"
```

```python
import requests

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

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

```javascript
const url = "https://eto.tools/api/v1/finance/12345678/sync";

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

#### Response

```
// ── Incremental ──
{
  "success": true,
  "mode": "incremental",
  "new_entries": 12,
  "total_fetched": 15,
  "affected_days": [1716508800, 1716595200]
}

// ── Single day ──
{
  "success": true,
  "mode": "day",
  "day_start": 1716508800,
  "day_end": 1716595199,
  "new_entries": 22,
  "total_fetched": 22
}

// ── Date range ──
{
  "success": true,
  "mode": "range",
  "start": 1716508800,
  "end": 1717113600,
  "deleted_stale": 145,
  "new_entries": 152
}
```

#### 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/finance/software-expenses

Get logged software expenses (subscriptions)

- Operation ID: `finance_software_expenses`
- Slug: `finance-software-expenses`
- Category: Finance
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-finance-software-expenses
- Markdown: https://eto.tools/dev/docs/finance/finance-software-expenses.md

Returns the software expenses you logged on the Finance dashboard — the subscriptions under Finance → Settings → Subscriptions (Canva, Printify, Eto, …) — and exactly what they cost inside a date window. The same numbers as the dashboard's "Software Expense" card and its breakdown. Zero Etsy API calls — reads from the Eto database. Instant.

These are ACCOUNT-level, not per shop, so they are NOT included in GET /finance/{shop_id}. To get profit after software, subtract total_cents from the shop's net_profit_cents.

━━━ WHAT YOU GET BACK (always returned) ━━━

start / end — the resolved window (unix seconds)
range — exact window: timezone, local-ISO bounds, label, server time
currency — the currency total_cents is in: your common currency from Finance settings, else the single currency all subscriptions share, else null (mixed)
common_currency — the common currency configured in Finance → Settings, or null
total_cents — everything charged inside the window, converted to `currency`
totals_by_currency — { "USD": 2598, "GBP": 999 } raw sums per native currency
charge_count — number of individual charges inside the window
subscription_count — number of subscriptions logged (all of them, not just active in window)
subscriptions[] — one entry per logged subscription:
  id, name, amount_cents, currency, frequency (weekly | biweekly | monthly | onetime), start_date (YYYY-MM-DD), conversion_rate (native → common), amount_converted_cents, charges_in_range, total_in_range_cents (converted), next_charge_date (YYYY-MM-DD or null), issue (only present when the entry is broken and never bills — e.g. missing amount/date)
scope — reminder that these are account-level costs, not per shop
warnings[] — caveats, e.g. mixed currencies with no common currency set
hint — only when nothing is logged yet, with where to add subscriptions

All amounts are COSTS (money out) returned as POSITIVE cents. Divide by 100 for display.

━━━ HOW CHARGES ARE COUNTED ━━━

Each subscription bills from its start_date on its schedule: weekly every 7 days, biweekly every 14, monthly on the same day each month (clamped to shorter months — the 31st bills on the 30th/28th), onetime exactly once on start_date. A charge is counted when its calendar date falls inside the window. So "this month" for a $12.99 monthly subscription started on the 15th is 1299 cents; "last_7_days" may be 0 if the billing day is not in that week. Identical to the dashboard.

━━━ HOW TO PICK THE DATE WINDOW ━━━

EASIEST — pass period=this_month (the default), today, yesterday, last_7_days, last_30_days or last_month. Or start_date=YYYY-MM-DD & end_date=YYYY-MM-DD (inclusive calendar days). Add timezone=Europe/London to resolve the window in a specific zone — the default is your primary connected shop's timezone, else UTC. Raw unix start/end are accepted too (max 366 days). The response echoes the resolved window in `range`, so you never have to guess which days were counted.

━━━ OPTIONAL ━━━

include=charges — also return charges[]: every individual charge inside the window (date, subscription_id, name, frequency, amount_cents, currency, converted_cents), sorted by date. charges_truncated is true if the list was cut at 2,000 entries (totals are always complete).

━━━ NOTES ━━━

Read-only. Subscriptions are added and edited on the Finance dashboard (eto.tools/dashboard/finances → Settings → Subscriptions).
Only YOUR account's subscriptions are ever returned — there is no id to look up.
An empty subscriptions[] with total_cents 0 is a valid answer: nothing is logged yet (the response carries a hint saying so).

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `period` | query | `string` | no | EASIEST — today, yesterday, last_7_days, last_30_days, this_month, last_month. Resolved server-side. Default: this_month. Default: `this_month`. |
| `start_date` | query | `string` | no | First calendar day "YYYY-MM-DD" (inclusive). |
| `end_date` | query | `string` | no | Last calendar day "YYYY-MM-DD" (inclusive). Defaults to start_date. |
| `timezone` | query | `string` | no | IANA timezone used to resolve period/dates into calendar days (default: your primary connected shop's timezone, else UTC). |
| `include` | query | `string` | no | Set to "charges" to also list every individual charge in the window. |
| `start` | query | `integer` | no | Advanced — unix timestamp, range start (inclusive). Prefer period/start_date. |
| `end` | query | `integer` | no | Advanced — unix timestamp, range end (exclusive). Prefer period/end_date. |

#### Request

```bash
# ── This month (default) ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/software-expenses"

# ── Last month ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/software-expenses?period=last_month"

# ── A specific window, in your own timezone ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/software-expenses?start_date=2026-07-01&end_date=2026-07-31&timezone=Europe/London"

# ── This month + every individual charge ──
curl -H "X-Eto-API-Key: eto_your_key" \
  "https://eto.tools/api/v1/finance/software-expenses?period=this_month&include=charges"
```

```python
import requests

url = "https://eto.tools/api/v1/finance/software-expenses"
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/finance/software-expenses";

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

```json
{
  "start": 1785542400,
  "end": 1788220800,
  "range": {
    "start": 1785542400,
    "end": 1788220800,
    "start_local": "2026-08-01T00:00:00+01:00",
    "end_local": "2026-09-01T00:00:00+01:00",
    "timezone": "Europe/London",
    "label": "this month (month-to-date)",
    "server_now_unix": 1787788800,
    "server_now_utc": "2026-08-23T00:00:00+00:00"
  },
  "currency": "GBP",
  "common_currency": "GBP",
  "total_cents": 2325,
  "totals_by_currency": { "USD": 1299, "GBP": 1299 },
  "charge_count": 2,
  "subscription_count": 3,
  "subscriptions": [
    {
      "id": "sub_1753000000000_ab12",
      "name": "Eto",
      "amount_cents": 1299,
      "currency": "GBP",
      "frequency": "monthly",
      "start_date": "2026-03-05",
      "conversion_rate": 1.0,
      "amount_converted_cents": 1299,
      "charges_in_range": 1,
      "total_in_range_cents": 1299,
      "next_charge_date": "2026-09-05"
    },
    {
      "id": "sub_1751000000000_cd34",
      "name": "Canva Pro",
      "amount_cents": 1299,
      "currency": "USD",
      "frequency": "monthly",
      "start_date": "2026-01-15",
      "conversion_rate": 0.79,
      "amount_converted_cents": 1026,
      "charges_in_range": 1,
      "total_in_range_cents": 1026,
      "next_charge_date": "2026-09-15"
    },
    {
      "id": "sub_1749000000000_ef56",
      "name": "Printify Premium",
      "amount_cents": 2499,
      "currency": "USD",
      "frequency": "monthly",
      "start_date": "2026-08-28",
      "conversion_rate": 0.79,
      "amount_converted_cents": 1974,
      "charges_in_range": 0,
      "total_in_range_cents": 0,
      "next_charge_date": "2026-08-28"
    }
  ],
  "scope": {
    "level": "account",
    "note": "Software expenses are logged per Eto account, not per shop, so they are NOT part of GET /api/v1/finance/{shop_id}. All amounts are costs (money out) returned as positive cents; subtract total_cents from a shop's net_profit_cents to get profit after software.",
    "manage_at": "https://eto.tools/dashboard/finances (Settings → Subscriptions)"
  },
  "warnings": []
}

// ── With include=charges (extra keys) ──
{
  "...": "...",
  "charges": [
    { "date": "2026-08-05", "subscription_id": "sub_1753000000000_ab12", "name": "Eto", "frequency": "monthly", "amount_cents": 1299, "currency": "GBP", "converted_cents": 1299 },
    { "date": "2026-08-15", "subscription_id": "sub_1751000000000_cd34", "name": "Canva Pro", "frequency": "monthly", "amount_cents": 1299, "currency": "USD", "converted_cents": 1026 }
  ],
  "charges_truncated": false
}

// ── Nothing logged yet ──
{
  "...": "...",
  "currency": null,
  "total_cents": 0,
  "subscription_count": 0,
  "subscriptions": [],
  "hint": "No software expenses are logged yet. Add them at eto.tools/dashboard/finances → Settings → Subscriptions and they will appear here."
}
```

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


## Images & Media

Listing images, videos and digital files.

### POST /api/v1/shops/{shop_id}/listings/{listing_id}/images

Upload listing image

- Operation ID: `listing_image_upload`
- Slug: `listing-image-upload`
- Category: Images & Media
- 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-image-upload
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-image-upload.md

Upload an image to a listing. Send as multipart/form-data with the image in the "image" field.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `image` | body | `file` | yes | Image file (JPEG, PNG, GIF) |
| `rank` | body | `integer` | no | Image display order (1-10) |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -F "image=@photo.jpg" -F "rank=1" "https://eto.tools/api/v1/shops/12345678/listings/1234567890/images"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/images"
headers = {"X-Eto-API-Key": "eto_your_key"}
files = {"image": open("photo.jpg", "rb")}
data = {"rank": "1"}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/images";
const form = new FormData();
form.append("image", fileInput.files[0]);
form.append("rank", "1");

const response = await fetch(url, {
  method: "POST",
  body: form,
  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}/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.

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/shops/{shop_id}/listings/{listing_id}/images/{image_id}

Delete listing image

- Operation ID: `listing_image_delete`
- Slug: `listing-image-delete`
- Category: Images & Media
- 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-image-delete
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-image-delete.md

Remove an image from a listing.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `image_id` | path | `integer` | yes | The image ID to delete |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/images/987654321"
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/images/987654321";

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}/images/{image_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.


### POST /api/v1/shops/{shop_id}/listings/{listing_id}/videos

Upload listing video

- Operation ID: `listing_video_upload`
- Slug: `listing-video-upload`
- Category: Images & Media
- 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-video-upload
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-video-upload.md

Upload a video to a listing.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `video` | body | `file` | yes | Video file |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -F "video=@clip.mp4" "https://eto.tools/api/v1/shops/12345678/listings/1234567890/videos"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/videos"
headers = {"X-Eto-API-Key": "eto_your_key"}
files = {"video": open("clip.mp4", "rb")}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/videos";
const form = new FormData();
form.append("video", fileInput.files[0]);

const response = await fetch(url, {
  method: "POST",
  body: form,
  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}/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.

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/shops/{shop_id}/listings/{listing_id}/videos/{video_id}

Delete listing video

- Operation ID: `listing_video_delete`
- Slug: `listing-video-delete`
- Category: Images & Media
- 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-video-delete
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-video-delete.md

Remove a video from a listing.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `video_id` | path | `integer` | yes | The video ID to delete |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/videos/987654321"
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/videos/987654321";

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}/videos/{video_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 · POST /api/v1/shops/{shop_id}/listings/{listing_id}/variation-images

Manage variation images

- Operation ID: `listing_variation_images_get`
- Slug: `listing-variation-images`
- Category: Images & Media
- 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-variation-images
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-variation-images.md
- Operation IDs by method: `listing_variation_images_get` = GET, `listing_variation_images_post` = POST

Get or upload images for listing variations.

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

#### Parameters

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

#### Request — GET

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/variation-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/shops/12345678/listings/1234567890/variation-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();
```

#### Request — POST

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

```python
import requests

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

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

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

const response = await fetch(url, {
  method: "POST",
  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}/variation-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.

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 · POST /api/v1/shops/{shop_id}/listings/{listing_id}/files

List or upload digital files

- Operation ID: `listing_file_upload_get`
- Slug: `listing-file-upload`
- Category: Images & Media
- 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-file-upload
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-file-upload.md
- Operation IDs by method: `listing_file_upload_get` = GET, `listing_file_upload_post` = POST

GET: List all digital files for a listing. POST: Upload a new digital file. Etsy docs: "Listing Files".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `file` | body | `file` | yes | Digital file to upload |
| `name` | body | `string` | no | Display name for the file |

#### Request — GET

```bash
curl -X GET "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files" \
  -H "X-Eto-API-Key: eto_your_key" \
  -F "file=@/path/to/photo.jpg"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files"
headers = {"X-Eto-API-Key": "eto_your_key"}
files = {"file": open("/path/to/photo.jpg", "rb")}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files";
const form = new FormData();
form.append("file", fileInput.files[0]);

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

#### Request — POST

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -F "file=@design.pdf" "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files"
headers = {"X-Eto-API-Key": "eto_your_key"}
files = {"file": open("design.pdf", "rb")}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files";
const form = new FormData();
form.append("file", fileInput.files[0]);

const response = await fetch(url, {
  method: "POST",
  body: form,
  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}/files` 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 · DELETE /api/v1/shops/{shop_id}/listings/{listing_id}/files/{file_id}

Get or delete a digital file

- Operation ID: `listing_file_detail_get`
- Slug: `listing-file-detail`
- Category: Images & Media
- 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-file-detail
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-file-detail.md
- Operation IDs by method: `listing_file_detail_get` = GET, `listing_file_detail_delete` = DELETE

Get details for or delete a specific digital file. Etsy docs: "Listing File".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `file_id` | path | `integer` | yes | The file ID |

#### Request — GET

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files/987654321"
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/files/987654321";

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 — DELETE

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/files/1"
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/files/1";

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}/files/{file_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/{image_id}

Get a single image

- Operation ID: `listing_image_detail`
- Slug: `listing-image-detail`
- Category: Images & Media
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-image-detail
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-image-detail.md

Get details for a specific listing image. Etsy docs: "Listing Image".

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

#### Parameters

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

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/images/987654321"
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/987654321";

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/{image_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}/videos/{video_id}

Get a single video

- Operation ID: `listing_video_detail`
- Slug: `listing-video-detail`
- Category: Images & Media
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-listing-video-detail
- Markdown: https://eto.tools/dev/docs/images-and-media/listing-video-detail.md

Get details for a specific listing video. Etsy docs: "Listing Video".

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

#### Parameters

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

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/videos/987654321"
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/987654321";

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/{video_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.


## Listing Properties

Category attributes on a listing.

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

Get or update listing property

- Operation ID: `listing_property_get`
- Slug: `listing-property`
- Category: Listing Properties
- 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-property
- Markdown: https://eto.tools/dev/docs/listing-properties/listing-property.md
- Operation IDs by method: `listing_property_get` = GET, `listing_property_put` = PUT

Manage specific properties (like color, size) for a listing.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `property_id` | path | `integer` | yes | The property ID |

#### Request — GET

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/properties/200"
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/properties/200";

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 "https://eto.tools/api/v1/shops/12345678/listings/1234567890/properties/200" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

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

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

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

const response = await fetch(url, {
  method: "PUT",
  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}/properties/{property_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}/properties/{property_id}

Get a listing property (public)

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

Get a specific property value for any listing. Etsy docs: "Listing Property".

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

#### Parameters

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

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/listings/1234567890/properties/200"
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/properties/200";

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}/properties/{property_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/shops/{shop_id}/listings/{listing_id}/properties

Get all listing properties

- Operation ID: `listing_properties_all`
- Slug: `listing-properties-all`
- Category: Listing Properties
- 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-properties-all
- Markdown: https://eto.tools/dev/docs/listing-properties/listing-properties-all.md

Get all properties for one of your listings. Etsy docs: "Listing Properties".

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

#### Parameters

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

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/properties"
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/properties";

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/shops/{shop_id}/listings/{listing_id}/properties` 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/shops/{shop_id}/listings/{listing_id}/properties/{property_id}

Delete a listing property

- Operation ID: `listing_property_delete`
- Slug: `listing-property-delete`
- Category: Listing Properties
- 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-property-delete
- Markdown: https://eto.tools/dev/docs/listing-properties/listing-property-delete.md

Remove a property value from your listing. Etsy docs: "Delete Listing Property".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `listing_id` | path | `integer` | yes | The listing ID |
| `property_id` | path | `integer` | yes | The property ID |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/properties/200"
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/properties/200";

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}/properties/{property_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.


## Shipping

Shipping profiles, destinations, upgrades and carriers.

### GET · POST /api/v1/shops/{shop_id}/shipping-profiles

List or create shipping profiles

- Operation ID: `shipping_profiles_get`
- Slug: `shipping-profiles`
- Category: Shipping
- 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-shipping-profiles
- Markdown: https://eto.tools/dev/docs/shipping/shipping-profiles.md
- Operation IDs by method: `shipping_profiles_get` = GET, `shipping_profiles_post` = POST

GET: List all shipping profiles for your shop. POST: Create a new shipping profile.

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/shipping-profiles`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/shipping-profiles"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles"
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/shipping-profiles";

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 — POST

```bash
curl -X POST "https://eto.tools/api/v1/shops/12345678/shipping-profiles" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

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

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles";

const response = await fetch(url, {
  method: "POST",
  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}/shipping-profiles` 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 · DELETE /api/v1/shops/{shop_id}/shipping-profiles/{profile_id}

Update or delete shipping profile

- Operation ID: `shipping_profile_detail_put`
- Slug: `shipping-profile-detail`
- Category: Shipping
- 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-shipping-profile-detail
- Markdown: https://eto.tools/dev/docs/shipping/shipping-profile-detail.md
- Operation IDs by method: `shipping_profile_detail_put` = PUT, `shipping_profile_detail_delete` = DELETE

Update or delete a specific shipping profile.

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/shipping-profiles/{profile_id}`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `profile_id` | path | `integer` | yes | The shipping profile ID |

#### Request — PUT

```bash
curl -X PUT -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"title": "Updated"}' "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "title": "Updated"
}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333";
const payload = {
  "title": "Updated"
};

const response = await fetch(url, {
  method: "PUT",
  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/shipping-profiles/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1"
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/shipping-profiles/1";

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}/shipping-profiles/{profile_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 · POST /api/v1/shops/{shop_id}/shipping-profiles/{profile_id}/destinations

List or add shipping destinations

- Operation ID: `shipping_destinations_get`
- Slug: `shipping-destinations`
- Category: Shipping
- 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-shipping-destinations
- Markdown: https://eto.tools/dev/docs/shipping/shipping-destinations.md
- Operation IDs by method: `shipping_destinations_get` = GET, `shipping_destinations_post` = POST

Get or add destination countries/regions for a shipping profile. Etsy docs: "Shipping Profile Destinations".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/shipping-profiles/{profile_id}/destinations`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `profile_id` | path | `integer` | yes | The shipping profile ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/destinations"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/destinations"
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/shipping-profiles/111222333/destinations";

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 — POST

```bash
curl -X POST "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/destinations" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/destinations"
headers = {"X-Eto-API-Key": "eto_your_key"}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/destinations";

const response = await fetch(url, {
  method: "POST",
  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}/shipping-profiles/{profile_id}/destinations` 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 · DELETE /api/v1/shops/{shop_id}/shipping-profiles/{profile_id}/destinations/{destination_id}

Update or delete a shipping destination

- Operation ID: `shipping_destination_detail_put`
- Slug: `shipping-destination-detail`
- Category: Shipping
- 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-shipping-destination-detail
- Markdown: https://eto.tools/dev/docs/shipping/shipping-destination-detail.md
- Operation IDs by method: `shipping_destination_detail_put` = PUT, `shipping_destination_detail_delete` = DELETE

Manage a specific shipping destination. Etsy docs: "Shipping Profile Destination".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/shipping-profiles/{profile_id}/destinations/{destination_id}`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `profile_id` | path | `integer` | yes | The shipping profile ID |
| `destination_id` | path | `integer` | yes | The destination ID |

#### Request — PUT

```bash
curl -X PUT -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"primary_cost": 500}' "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/destinations/77777"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/destinations/77777"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "primary_cost": 500
}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/destinations/77777";
const payload = {
  "primary_cost": 500
};

const response = await fetch(url, {
  method: "PUT",
  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/shipping-profiles/1/destinations/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/destinations/1"
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/shipping-profiles/1/destinations/1";

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}/shipping-profiles/{profile_id}/destinations/{destination_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 · POST /api/v1/shops/{shop_id}/shipping-profiles/{profile_id}/upgrades

List or add shipping upgrades

- Operation ID: `shipping_upgrades_get`
- Slug: `shipping-upgrades`
- Category: Shipping
- 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-shipping-upgrades
- Markdown: https://eto.tools/dev/docs/shipping/shipping-upgrades.md
- Operation IDs by method: `shipping_upgrades_get` = GET, `shipping_upgrades_post` = POST

Get or add shipping speed upgrades (e.g. express, priority). Etsy docs: "Shipping Profile Upgrades".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/shipping-profiles/{profile_id}/upgrades`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `profile_id` | path | `integer` | yes | The shipping profile ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/upgrades"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/upgrades"
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/shipping-profiles/111222333/upgrades";

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 — POST

```bash
curl -X POST "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/upgrades" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/upgrades"
headers = {"X-Eto-API-Key": "eto_your_key"}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/upgrades";

const response = await fetch(url, {
  method: "POST",
  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}/shipping-profiles/{profile_id}/upgrades` 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 · DELETE /api/v1/shops/{shop_id}/shipping-profiles/{profile_id}/upgrades/{upgrade_id}

Get, update, or delete a shipping upgrade

- Operation ID: `shipping_upgrade_detail_get`
- Slug: `shipping-upgrade-detail`
- Category: Shipping
- 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-shipping-upgrade-detail
- Markdown: https://eto.tools/dev/docs/shipping/shipping-upgrade-detail.md
- Operation IDs by method: `shipping_upgrade_detail_get` = GET, `shipping_upgrade_detail_put` = PUT, `shipping_upgrade_detail_delete` = DELETE

Manage a specific shipping speed upgrade. Etsy docs: "Shipping Profile Upgrade".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/shipping-profiles/{profile_id}/upgrades/{upgrade_id}`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `profile_id` | path | `integer` | yes | The shipping profile ID |
| `upgrade_id` | path | `integer` | yes | The upgrade ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/upgrades/88888"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/111222333/upgrades/88888"
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/shipping-profiles/111222333/upgrades/88888";

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 "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/upgrades/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/upgrades/1"
headers = {"X-Eto-API-Key": "eto_your_key"}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/upgrades/1";

const response = await fetch(url, {
  method: "PUT",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
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/shipping-profiles/1/upgrades/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/shipping-profiles/1/upgrades/1"
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/shipping-profiles/1/upgrades/1";

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}/shipping-profiles/{profile_id}/upgrades/{upgrade_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/shipping-carriers

Get shipping carriers

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

List all available shipping carriers (USPS, FedEx, DHL, etc.). Etsy docs: "Shipping Carriers".

Proxied to the Etsy API at `/v3/application/shipping-carriers`. Fields Etsy returns are passed through untouched.

#### Parameters

None. Send the request with only the API key header.

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shipping-carriers"
```

```python
import requests

url = "https://eto.tools/api/v1/shipping-carriers"
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/shipping-carriers";

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/shipping-carriers` 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.


## Categories

The Etsy taxonomy and the properties each category allows.

### GET /api/v1/categories

Get product categories

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

Get the full product category tree (e.g. Jewelry, Clothing, Home Decor). Use category IDs when creating listings to place them in the right section. Etsy docs: "Seller Taxonomy Nodes".

Proxied to the Etsy API at `/v3/application/seller-taxonomy/nodes`. Fields Etsy returns are passed through untouched.

#### Parameters

None. Send the request with only the API key header.

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/categories"
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/categories";

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/seller-taxonomy/nodes` 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/categories/{category_id}/properties

Get category properties

- Operation ID: `category_properties`
- Slug: `category-properties`
- Category: Categories
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-category-properties
- Markdown: https://eto.tools/dev/docs/categories/category-properties.md

Get the required and optional fields for a category — like size, color, or material options that buyers can filter by. Etsy docs: "Taxonomy Node Properties".

Proxied to the Etsy API at `/v3/application/seller-taxonomy/nodes/{category_id}/properties`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `category_id` | path | `integer` | yes | The category ID (from /categories) |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/categories/1/properties"
```

```python
import requests

url = "https://eto.tools/api/v1/categories/1/properties"
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/categories/1/properties";

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/seller-taxonomy/nodes/{category_id}/properties` 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/buyer-categories

Get buyer categories

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

Get the category tree from a buyer's perspective — how shoppers browse Etsy. Etsy docs: "Buyer Taxonomy Nodes".

Proxied to the Etsy API at `/v3/application/buyer-taxonomy/nodes`. Fields Etsy returns are passed through untouched.

#### Parameters

None. Send the request with only the API key header.

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/buyer-categories"
```

```python
import requests

url = "https://eto.tools/api/v1/buyer-categories"
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/buyer-categories";

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/buyer-taxonomy/nodes` 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/buyer-categories/{category_id}/properties

Get buyer category properties

- Operation ID: `buyer_category_properties`
- Slug: `buyer-category-properties`
- Category: Categories
- Authentication: API key only. Reads public marketplace data, so no store needs to be connected.
- HTML: https://eto.tools/dev/docs/#ep-buyer-category-properties
- Markdown: https://eto.tools/dev/docs/categories/buyer-category-properties.md

Get filterable properties for a buyer category. Etsy docs: "Buyer Taxonomy Properties".

Proxied to the Etsy API at `/v3/application/buyer-taxonomy/nodes/{category_id}/properties`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `category_id` | path | `integer` | yes | The buyer category ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/buyer-categories/1/properties"
```

```python
import requests

url = "https://eto.tools/api/v1/buyer-categories/1/properties"
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/buyer-categories/1/properties";

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/buyer-taxonomy/nodes/{category_id}/properties` 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.


## User

The authenticated Etsy user and the shops they own.

### GET /api/v1/users/me

Get current user

- Operation ID: `users_me`
- Slug: `users-me`
- Category: User
- 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-users-me
- Markdown: https://eto.tools/dev/docs/user/users-me.md

Retrieve the Etsy user profile for the authenticated store.

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

#### Parameters

None. Send the request with only the API key header.

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/users/me"
```

```python
import requests

url = "https://eto.tools/api/v1/users/me"
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/users/me";

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/users/me` 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/users/{user_id}/shops

Get user shops

- Operation ID: `user_shops`
- Slug: `user-shops`
- Category: User
- 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-user-shops
- Markdown: https://eto.tools/dev/docs/user/user-shops.md

List shops owned by a specific user.

Proxied to the Etsy API at `/v3/application/users/{user_id}/shops`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `user_id` | path | `integer` | yes | The Etsy user ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/users/123456789/shops"
```

```python
import requests

url = "https://eto.tools/api/v1/users/123456789/shops"
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/users/123456789/shops";

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/users/{user_id}/shops` 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/users/{user_id}

Get user profile

- Operation ID: `user_profile`
- Slug: `user-profile`
- Category: User
- 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-user-profile
- Markdown: https://eto.tools/dev/docs/user/user-profile.md

Get a user's Etsy profile by ID. Etsy docs: "Get User".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `user_id` | path | `integer` | yes | The Etsy user ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/users/123456789"
```

```python
import requests

url = "https://eto.tools/api/v1/users/123456789"
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/users/123456789";

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/users/{user_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.


## Shop Policies

Return policies, shop sections and listing requirements.

### GET /api/v1/shops/{shop_id}/return-policies

Get return policies

- Operation ID: `shop_return_policies`
- Slug: `shop-return-policies`
- Category: Shop Policies
- 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-return-policies
- Markdown: https://eto.tools/dev/docs/shop-policies/shop-return-policies.md

Retrieve return policy details for your shop.

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/return-policies"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/return-policies"
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/return-policies";

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/shops/{shop_id}/policies/return` 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/shops/{shop_id}/production-partners

Get production partners

- Operation ID: `shop_production_partners`
- Slug: `shop-production-partners`
- Category: Shop Policies
- 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-production-partners
- Markdown: https://eto.tools/dev/docs/shop-policies/shop-production-partners.md

List production partners (e.g. print-on-demand services) for your shop.

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/production-partners`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/production-partners"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/production-partners"
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/production-partners";

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/shops/{shop_id}/production-partners` 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 · POST /api/v1/shops/{shop_id}/listing-requirements

Listing requirements

- Operation ID: `shop_listing_requirements_get`
- Slug: `shop-listing-requirements`
- Category: Shop Policies
- 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-requirements
- Markdown: https://eto.tools/dev/docs/shop-policies/shop-listing-requirements.md
- Operation IDs by method: `shop_listing_requirements_get` = GET, `shop_listing_requirements_post` = POST

Get or create listing readiness requirements — the checklist items a listing must complete before going live. Etsy docs: "Readiness State Definitions".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/readiness-state-definitions`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/listing-requirements"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listing-requirements"
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/listing-requirements";

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 — POST

```bash
curl -X POST "https://eto.tools/api/v1/shops/12345678/listing-requirements" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

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

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listing-requirements";

const response = await fetch(url, {
  method: "POST",
  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}/readiness-state-definitions` 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 /api/v1/shops/{shop_id}/return-policies

Create return policy

- Operation ID: `return_policy_create`
- Slug: `return-policy-create`
- Category: Shop Policies
- 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-return-policy-create
- Markdown: https://eto.tools/dev/docs/shop-policies/return-policy-create.md

Create a new return policy for your shop. Etsy docs: "Create Return Policy".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"accepts_returns": true, "return_deadline": 30}' "https://eto.tools/api/v1/shops/12345678/return-policies"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/return-policies"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "accepts_returns": True,
    "return_deadline": 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/shops/12345678/return-policies";
const payload = {
  "accepts_returns": true,
  "return_deadline": 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}/policies/return` 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 · DELETE /api/v1/shops/{shop_id}/return-policies/{policy_id}

Get, update, or delete a return policy

- Operation ID: `return_policy_detail_get`
- Slug: `return-policy-detail`
- Category: Shop Policies
- 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-return-policy-detail
- Markdown: https://eto.tools/dev/docs/shop-policies/return-policy-detail.md
- Operation IDs by method: `return_policy_detail_get` = GET, `return_policy_detail_put` = PUT, `return_policy_detail_delete` = DELETE

Manage a specific return policy. Etsy docs: "Shop Return Policy".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/policies/return/{policy_id}`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `policy_id` | path | `integer` | yes | The return policy ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/return-policies/54321"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/return-policies/54321"
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/return-policies/54321";

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 "https://eto.tools/api/v1/shops/12345678/return-policies/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/return-policies/1"
headers = {"X-Eto-API-Key": "eto_your_key"}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/return-policies/1";

const response = await fetch(url, {
  method: "PUT",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
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/return-policies/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/return-policies/1"
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/return-policies/1";

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}/policies/return/{policy_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.


### POST /api/v1/shops/{shop_id}/return-policies/consolidate

Consolidate return policies

- Operation ID: `return_policy_consolidate`
- Slug: `return-policy-consolidate`
- Category: Shop Policies
- 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-return-policy-consolidate
- Markdown: https://eto.tools/dev/docs/shop-policies/return-policy-consolidate.md

Merge multiple return policies into one. Etsy docs: "Consolidate Return Policies".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/policies/return/consolidate`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/return-policies/consolidate"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/return-policies/consolidate"
headers = {"X-Eto-API-Key": "eto_your_key"}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/return-policies/consolidate";

const response = await fetch(url, {
  method: "POST",
  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}/policies/return/consolidate` 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/shops/{shop_id}/return-policies/{policy_id}/listings

Get listings with a return policy

- Operation ID: `return_policy_listings`
- Slug: `return-policy-listings`
- Category: Shop Policies
- 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-return-policy-listings
- Markdown: https://eto.tools/dev/docs/shop-policies/return-policy-listings.md

Get all listings that use a specific return policy. Etsy docs: "Listings By Return Policy".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `policy_id` | path | `integer` | yes | The return policy ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/return-policies/54321/listings"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/return-policies/54321/listings"
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/return-policies/54321/listings";

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/shops/{shop_id}/policies/return/{policy_id}/listings` 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/shops/{shop_id}/holiday-preferences

Get holiday preferences

- Operation ID: `shop_holidays`
- Slug: `shop-holidays`
- Category: Shop Policies
- 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-holidays
- Markdown: https://eto.tools/dev/docs/shop-policies/shop-holidays.md

Get your shop's holiday settings (when you're on vacation). Etsy docs: "Holiday Preferences".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/holiday-preferences`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/holiday-preferences"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/holiday-preferences"
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/holiday-preferences";

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/shops/{shop_id}/holiday-preferences` 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/shops/{shop_id}/holiday-preferences/{holiday_id}

Update a holiday preference

- Operation ID: `shop_holiday_update`
- Slug: `shop-holiday-update`
- Category: Shop Policies
- 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-holiday-update
- Markdown: https://eto.tools/dev/docs/shop-policies/shop-holiday-update.md

Update vacation/holiday settings. Etsy docs: "Update Holiday Preferences".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/holiday-preferences/{holiday_id}`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `holiday_id` | path | `integer` | yes | The holiday ID |

#### Request

```bash
curl -X PUT -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"is_working": false}' "https://eto.tools/api/v1/shops/12345678/holiday-preferences/1"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/holiday-preferences/1"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "is_working": False
}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/holiday-preferences/1";
const payload = {
  "is_working": false
};

const response = await fetch(url, {
  method: "PUT",
  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}/holiday-preferences/{holiday_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 · PUT · DELETE /api/v1/shops/{shop_id}/listing-requirements/{requirement_id}

Get, update, or delete a listing requirement

- Operation ID: `listing_requirement_detail_get`
- Slug: `listing-requirement-detail`
- Category: Shop Policies
- 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-requirement-detail
- Markdown: https://eto.tools/dev/docs/shop-policies/listing-requirement-detail.md
- Operation IDs by method: `listing_requirement_detail_get` = GET, `listing_requirement_detail_put` = PUT, `listing_requirement_detail_delete` = DELETE

Manage a specific listing readiness requirement. Etsy docs: "Readiness State Definition".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/readiness-state-definitions/{requirement_id}`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `requirement_id` | path | `integer` | yes | The requirement ID |

#### Request — GET

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/listing-requirements/99999"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listing-requirements/99999"
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/listing-requirements/99999";

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 "https://eto.tools/api/v1/shops/12345678/listing-requirements/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listing-requirements/1"
headers = {"X-Eto-API-Key": "eto_your_key"}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/listing-requirements/1";

const response = await fetch(url, {
  method: "PUT",
  headers: {
    "X-Eto-API-Key": "eto_your_key",
  },
});
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/listing-requirements/1" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listing-requirements/1"
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/listing-requirements/1";

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}/readiness-state-definitions/{requirement_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/stores/{shop_id}/shipping-profiles/live

Get current shipping profiles (live, no cache)

- Operation ID: `shipping_profiles_live`
- Slug: `shipping-profiles-live`
- Category: Shop Policies
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-shipping-profiles-live
- Markdown: https://eto.tools/dev/docs/shop-policies/shipping-profiles-live.md

Returns this shop's shipping profiles RIGHT NOW — fetched live from Etsy on every call, never from the local cache. Use this when you need the absolute latest IDs to put into shops[].shipping_profile_id when calling POST /api/v1/listings/create.

One Etsy API call per request. Response is a flat list — pick the shipping_profile_id you want.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID. Get the list from GET /api/v1/stores. |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/shipping-profiles/live"
```

```python
import requests

url = "https://eto.tools/api/v1/stores/12345678/shipping-profiles/live"
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/stores/12345678/shipping-profiles/live";

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

```json
{
  "shop_id": "12345678",
  "fetched_at": "2026-04-19T14:30:00Z",
  "count": 2,
  "shipping_profiles": [
    {
      "shipping_profile_id": 291257702631,
      "title": "Standard Shipping",
      "min_processing_days": 1,
      "max_processing_days": 3,
      "processing_days_display_label": "1-3 business days",
      "origin_country_iso": "US",
      "origin_postal_code": "10001",
      "profile_type": "manual",
      "domestic_handling_fee": 0.00,
      "international_handling_fee": 0.00
    }
  ]
}
```

#### 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/stores/{shop_id}/return-policies/live

Get current return policies (live, no cache)

- Operation ID: `return_policies_live`
- Slug: `return-policies-live`
- Category: Shop Policies
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-return-policies-live
- Markdown: https://eto.tools/dev/docs/shop-policies/return-policies-live.md

Returns this shop's return policies RIGHT NOW — fetched live from Etsy on every call. No cache, no staleness. Use this to get the return_policy_id to plug into shops[].return_policy_id when calling POST /api/v1/listings/create.

One Etsy API call per request.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID. |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/return-policies/live"
```

```python
import requests

url = "https://eto.tools/api/v1/stores/12345678/return-policies/live"
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/stores/12345678/return-policies/live";

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

```json
{
  "shop_id": "12345678",
  "fetched_at": "2026-04-19T14:30:00Z",
  "count": 1,
  "return_policies": [
    {
      "return_policy_id": 1435074483195,
      "accepts_returns": true,
      "accepts_exchanges": true,
      "return_deadline": 30
    }
  ]
}
```

#### 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/stores/{shop_id}/processing-profiles/live

Get current processing profiles (live, no cache)

- Operation ID: `processing_profiles_live`
- Slug: `processing-profiles-live`
- Category: Shop Policies
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-processing-profiles-live
- Markdown: https://eto.tools/dev/docs/shop-policies/processing-profiles-live.md

Returns this shop's processing profiles (Etsy "readiness state definitions") RIGHT NOW — fetched live from Etsy on every call. No cache. Use this to get the processing_profile_id (a.k.a. readiness_state_id) for shops[].processing_profile_id when calling POST /api/v1/listings/create.

One Etsy API call per request.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID. |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/processing-profiles/live"
```

```python
import requests

url = "https://eto.tools/api/v1/stores/12345678/processing-profiles/live"
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/stores/12345678/processing-profiles/live";

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

```json
{
  "shop_id": "12345678",
  "fetched_at": "2026-04-19T14:30:00Z",
  "count": 1,
  "processing_profiles": [
    {
      "readiness_state_id": 1456101932490,
      "readiness_state": "ready_to_ship",
      "min_processing_days": 1,
      "max_processing_days": 3,
      "processing_days_display_label": "1-3 business days"
    }
  ]
}
```

#### 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/stores/{shop_id}/shop-sections/live

Get current shop sections (live, no cache)

- Operation ID: `shop_sections_live`
- Slug: `shop-sections-live`
- Category: Shop Policies
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-shop-sections-live
- Markdown: https://eto.tools/dev/docs/shop-policies/shop-sections-live.md

Returns this shop's sections RIGHT NOW — fetched live from Etsy on every call. No cache. Use this to get the shop_section_id to plug into shops[].shop_section_id when calling POST /api/v1/listings/create.

One Etsy API call per request.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your connected Etsy shop ID. |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/stores/12345678/shop-sections/live"
```

```python
import requests

url = "https://eto.tools/api/v1/stores/12345678/shop-sections/live"
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/stores/12345678/shop-sections/live";

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

```json
{
  "shop_id": "12345678",
  "fetched_at": "2026-04-19T14:30:00Z",
  "count": 2,
  "shop_sections": [
    {
      "shop_section_id": 56909368,
      "title": "New Arrivals",
      "rank": 1,
      "user_id": 99887766,
      "active_listing_count": 15
    }
  ]
}
```

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


## Orders

Receipts and fulfilment, both Eto’s synced copy and the live Etsy passthrough.

### GET /api/v1/shops/{shop_id}/orders

List shop orders

- Operation ID: `shop_orders`
- Slug: `shop-orders`
- Category: Orders
- 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-orders
- Markdown: https://eto.tools/dev/docs/orders/shop-orders.md

Get all orders for your shop. Supports filtering by date and pagination. Etsy docs: "Shop Receipts".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `limit` | query | `integer` | no | Number of orders (max 100) Default: `25`. |
| `offset` | query | `integer` | no | Pagination offset Default: `0`. |
| `min_created` | query | `integer` | no | Earliest order date (unix seconds) |
| `max_created` | query | `integer` | no | Latest order date (unix seconds) |
| `sort_on` | query | `string` | no | Sort field Default: `created`. |
| `sort_order` | query | `string` | no | Sort direction Default: `desc`. |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/orders?limit=10"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/orders?limit=10"
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/orders?limit=10";

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/shops/{shop_id}/receipts` 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/shops/{shop_id}/orders/{order_id}

Get order details

- Operation ID: `shop_order_detail`
- Slug: `shop-order-detail`
- Category: Orders
- 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-order-detail
- Markdown: https://eto.tools/dev/docs/orders/shop-order-detail.md

Get full details for a specific order including items, shipping, and payment info. Etsy docs: "Shop Receipt by ID".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `order_id` | path | `integer` | yes | The order ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/orders/3344556677"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/orders/3344556677"
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/orders/3344556677";

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/shops/{shop_id}/receipts/{order_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.


### POST /api/v1/shops/{shop_id}/orders/{order_id}/tracking

Update order tracking

- Operation ID: `shop_order_tracking`
- Slug: `shop-order-tracking`
- Category: Orders
- 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-order-tracking
- Markdown: https://eto.tools/dev/docs/orders/shop-order-tracking.md

Add or update tracking information for an order so the buyer can track their shipment. Etsy docs: "Receipt Tracking".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/receipts/{order_id}/tracking`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `order_id` | path | `integer` | yes | The order ID |
| `tracking_code` | body | `string` | yes | Tracking number |
| `carrier_name` | body | `string` | yes | Shipping carrier name |

#### Request

```bash
curl -X POST -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"tracking_code": "1Z999AA10123456784", "carrier_name": "ups"}' "https://eto.tools/api/v1/shops/12345678/orders/3344556677/tracking"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/orders/3344556677/tracking"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "tracking_code": "1Z999AA10123456784",
    "carrier_name": "ups"
}

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/orders/3344556677/tracking";
const payload = {
  "tracking_code": "1Z999AA10123456784",
  "carrier_name": "ups"
};

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}/receipts/{order_id}/tracking` 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/shops/{shop_id}/orders/{order_id}/update

Update an order

- Operation ID: `order_update`
- Slug: `order-update`
- Category: Orders
- 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-order-update
- Markdown: https://eto.tools/dev/docs/orders/order-update.md

Update order details like notes or status. Etsy docs: "Update Shop Receipt".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `order_id` | path | `integer` | yes | The order ID |

#### Request

```bash
curl -X PUT -H "X-Eto-API-Key: eto_your_key" -H "Content-Type: application/json" -d '{"was_shipped": true}' "https://eto.tools/api/v1/shops/12345678/orders/3344556677/update"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/orders/3344556677/update"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "was_shipped": True
}

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

```javascript
const url = "https://eto.tools/api/v1/shops/12345678/orders/3344556677/update";
const payload = {
  "was_shipped": true
};

const response = await fetch(url, {
  method: "PUT",
  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}/receipts/{order_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/shops/{shop_id}/orders/{order_id}/listings

Get listings in an order

- Operation ID: `order_listings`
- Slug: `order-listings`
- Category: Orders
- 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-order-listings
- Markdown: https://eto.tools/dev/docs/orders/order-listings.md

Get the listings that were purchased in a specific order. Etsy docs: "Listings By Receipt".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `order_id` | path | `integer` | yes | The order ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/orders/3344556677/listings"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/orders/3344556677/listings"
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/orders/3344556677/listings";

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/shops/{shop_id}/receipts/{order_id}/listings` 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/shops/{shop_id}/orders/{order_id}/payments

Get order payments

- Operation ID: `order_payments`
- Slug: `order-payments`
- Category: Orders
- 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-order-payments
- Markdown: https://eto.tools/dev/docs/orders/order-payments.md

Get payment details for a specific order. Etsy docs: "Payment By Receipt ID".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/receipts/{order_id}/payments`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `order_id` | path | `integer` | yes | The order ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/orders/3344556677/payments"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/orders/3344556677/payments"
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/orders/3344556677/payments";

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/shops/{shop_id}/receipts/{order_id}/payments` 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/orders/{shop_id}

List orders for a store

- Operation ID: `orders_list`
- Slug: `orders-list`
- Category: Orders
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-orders-list
- Markdown: https://eto.tools/dev/docs/orders/orders-list.md

List receipts/orders for a connected store (newest first by default — so for "my latest order" just call this, no date filter). Triggers a background sync so data stays fresh. To filter by date, pass a friendly `period` or `start_date`/`end_date` (resolved server-side in the shop timezone) — do NOT compute unix timestamps. Every order includes `created_iso` (UTC) and `created_local` (shop/your timezone), and the response carries `server_now_utc` + `timezone`, so you never mis-read a raw timestamp or think an order is "in the future".

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Connected shop id |
| `limit` | query | `integer` | no | Page size (default 25) |
| `offset` | query | `integer` | no | Pagination offset |
| `period` | query | `string` | no | Friendly date filter: today, yesterday, last_7_days, last_30_days, this_month, last_month. |
| `start_date` | query | `string` | no | Filter from this calendar day "YYYY-MM-DD" (inclusive). |
| `end_date` | query | `string` | no | Filter to this calendar day "YYYY-MM-DD" (inclusive). |
| `timezone` | query | `string` | no | IANA tz for resolving period/dates + formatting created_local (default: shop tz). |
| `min_created` | query | `integer` | no | Advanced: unix lower bound on order creation. Prefer period/start_date. |
| `max_created` | query | `integer` | no | Advanced: unix upper bound on order creation. Prefer period/end_date. |
| `sort_on` | query | `string` | no | Sort field (default "created") |
| `sort_order` | query | `string` | no | asc or desc (default desc) |

#### Request

```bash
curl -X GET "https://eto.tools/api/v1/orders/12345678?limit=25&offset=0" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/orders/12345678"
headers = {"X-Eto-API-Key": "eto_your_key"}
params = {
    "limit": 25,
    "offset": 0
}

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

```javascript
const url = "https://eto.tools/api/v1/orders/12345678?limit=25&offset=0";

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-orders-list) shows the fields it returns.

#### 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/orders/{shop_id}/{receipt_id}

Get one order

- Operation ID: `order_detail`
- Slug: `order-detail`
- Category: Orders
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-order-detail
- Markdown: https://eto.tools/dev/docs/orders/order-detail.md

Get a single order (receipt) with its transactions for a connected store. Also returns a `profit` object = THIS order's own profit: gross_profit_cents (its gross sale minus its OWN fees, incl remitted tax), fees_cents, fees_breakdown, cogs_cents, net_profit_cents. This is the answer to "profit for this product/order" — it does NOT subtract shop-wide ad spend or other orders' refunds/cancellations. (profit is null until a finance_sync has run.)

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Connected shop id |
| `receipt_id` | path | `integer` | yes | Etsy receipt id |

#### Request

```bash
curl -X GET "https://eto.tools/api/v1/orders/12345678/987654321" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/orders/12345678/987654321"
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/orders/12345678/987654321";

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-order-detail) shows the fields it returns.

#### 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/orders/{shop_id}/sync

Sync orders from Etsy

- Operation ID: `orders_sync`
- Slug: `orders-sync`
- Category: Orders
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-orders-sync
- Markdown: https://eto.tools/dev/docs/orders/orders-sync.md

Force a fresh sync of orders for a connected store from Etsy.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Connected shop id |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/orders/12345678/sync" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

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

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

```javascript
const url = "https://eto.tools/api/v1/orders/12345678/sync";

const response = await fetch(url, {
  method: "POST",
  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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-orders-sync) shows the fields it returns.

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


## Finances

Raw Etsy ledger data: transactions, payments, sales.

### GET /api/v1/shops/{shop_id}/finance/transactions

Get financial transactions

- Operation ID: `shop_finance_transactions`
- Slug: `shop-finance-transactions`
- Category: Finances
- 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-finance-transactions
- Markdown: https://eto.tools/dev/docs/finances/shop-finance-transactions.md

Get all financial transactions — charges, fees, refunds, and deposits. This is your shop's payment ledger. Etsy docs: "Payment Account Ledger Entries".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/payment-account/ledger-entries`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `min_created` | query | `integer` | no | Earliest date (unix seconds) |
| `max_created` | query | `integer` | no | Latest date (unix seconds) |
| `limit` | query | `integer` | no | Number of results (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/shops/12345678/finance/transactions?limit=10"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/finance/transactions?limit=10"
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/finance/transactions?limit=10";

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/shops/{shop_id}/payment-account/ledger-entries` 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/shops/{shop_id}/finance/payments

Get payment details

- Operation ID: `shop_finance_payments`
- Slug: `shop-finance-payments`
- Category: Finances
- 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-finance-payments
- Markdown: https://eto.tools/dev/docs/finances/shop-finance-payments.md

Get detailed payment info for specific financial transactions. Etsy docs: "Ledger Entry Payments".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/payment-account/ledger-entries/payments`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `transaction_ids` | query | `string` | yes | Comma-separated transaction IDs |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/finance/payments?transaction_ids=123,456"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/finance/payments?transaction_ids=123,456"
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/finance/payments?transaction_ids=123,456";

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/shops/{shop_id}/payment-account/ledger-entries/payments` 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/shops/{shop_id}/finance/sales

Get sales history

- Operation ID: `shop_finance_sales`
- Slug: `shop-finance-sales`
- Category: Finances
- 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-finance-sales
- Markdown: https://eto.tools/dev/docs/finances/shop-finance-sales.md

Get individual sale records — each item sold, its price, and the buyer. Etsy docs: "Shop Transactions".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `limit` | query | `integer` | no | Number of sales (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/shops/12345678/finance/sales?limit=10"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/finance/sales?limit=10"
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/finance/sales?limit=10";

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/shops/{shop_id}/transactions` 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/shops/{shop_id}/listings/{listing_id}/sales

Get sales for a listing

- Operation ID: `listing_sales`
- Slug: `listing-sales`
- Category: Finances
- 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-sales
- Markdown: https://eto.tools/dev/docs/finances/listing-sales.md

Get all sale transactions for a specific listing. Etsy docs: "Transactions By Listing".

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

#### Parameters

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

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/listings/1234567890/sales"
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/sales";

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/shops/{shop_id}/listings/{listing_id}/transactions` 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/shops/{shop_id}/orders/{order_id}/transactions

Get transactions for an order

- Operation ID: `order_transactions`
- Slug: `order-transactions`
- Category: Finances
- 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-order-transactions
- Markdown: https://eto.tools/dev/docs/finances/order-transactions.md

Get individual sale items within a specific order. Etsy docs: "Transactions By Receipt".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/receipts/{order_id}/transactions`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `order_id` | path | `integer` | yes | The order ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/orders/3344556677/transactions"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/orders/3344556677/transactions"
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/orders/3344556677/transactions";

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/shops/{shop_id}/receipts/{order_id}/transactions` 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/shops/{shop_id}/finance/sales/{transaction_id}

Get a single sale

- Operation ID: `single_transaction`
- Slug: `single-transaction`
- Category: Finances
- 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-single-transaction
- Markdown: https://eto.tools/dev/docs/finances/single-transaction.md

Get details for one specific sale transaction. Etsy docs: "Shop Receipt Transaction".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `transaction_id` | path | `integer` | yes | The transaction ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/finance/sales/4455667788"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/finance/sales/4455667788"
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/finance/sales/4455667788";

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/shops/{shop_id}/transactions/{transaction_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/shops/{shop_id}/finance/transactions/{entry_id}

Get a single financial transaction

- Operation ID: `single_ledger_entry`
- Slug: `single-ledger-entry`
- Category: Finances
- 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-single-ledger-entry
- Markdown: https://eto.tools/dev/docs/finances/single-ledger-entry.md

Get details for one ledger entry. Etsy docs: "Ledger Entry".

Proxied to the Etsy API at `/v3/application/shops/{shop_id}/payment-account/ledger-entries/{entry_id}`. Fields Etsy returns are passed through untouched.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |
| `entry_id` | path | `integer` | yes | The ledger entry ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/finance/transactions/9988776655"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/finance/transactions/9988776655"
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/finance/transactions/9988776655";

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/shops/{shop_id}/payment-account/ledger-entries/{entry_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/shops/{shop_id}/finance/all-payments

Get all payments

- Operation ID: `shop_all_payments`
- Slug: `shop-all-payments`
- Category: Finances
- 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-all-payments
- Markdown: https://eto.tools/dev/docs/finances/shop-all-payments.md

Get all payment records for your shop. Etsy docs: "Shop Payments".

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

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/finance/all-payments"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/finance/all-payments"
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/finance/all-payments";

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/shops/{shop_id}/payments` 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.


## Analytics

Shop traffic statistics.

### GET /api/v1/shops/{shop_id}/stats/traffic

Shop views & favorites (lifetime)

- Operation ID: `shop_traffic_stats`
- Slug: `shop-traffic-stats`
- Category: Analytics
- 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-traffic-stats
- Markdown: https://eto.tools/dev/docs/analytics/shop-traffic-stats.md

Lifetime VIEWS and FAVORITES aggregated across your active listings, plus a top-listings-by-views leaderboard, total, and average. NOTE: Etsy's public API does not expose day-by-day traffic (Stats are dashboard-only), so these are cumulative figures, not per-day. No date range needed.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Your Etsy shop ID |

#### Request

```bash
curl -H "X-Eto-API-Key: eto_your_key" "https://eto.tools/api/v1/shops/12345678/stats/traffic"
```

```python
import requests

url = "https://eto.tools/api/v1/shops/12345678/stats/traffic"
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/stats/traffic";

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-shop-traffic-stats) shows the fields it returns.

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


## Stores

The Etsy stores connected to your Eto account.

### GET /api/v1/stores

List connected stores

- Operation ID: `stores_list`
- Slug: `stores-list`
- Category: Stores
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-stores-list
- Markdown: https://eto.tools/dev/docs/stores/stores-list.md

List every Etsy store connected to your Eto account, with shop_id, name and basic status. Start here: the shop_id values are what every shop-scoped endpoint needs. `token_valid` false means the store is connected but its Etsy authorisation has lapsed, so shop-scoped calls will answer STORE_TOKEN_EXPIRED until it is reconnected. Verified against developer_api/views_stores.py.

#### Parameters

None. Send the request with only the API key header.

#### Request

```bash
curl -X GET "https://eto.tools/api/v1/stores" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/stores"
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/stores";

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

```json
{
  "stores": [
    {
      "shop_id": "12345678",
      "shop_name": "MyShop",
      "is_connected": true,
      "token_valid": true,
      "connected_at": "2026-04-19T14:30:00+00:00"
    }
  ]
}
```

#### 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/stores/{shop_id}

Get a connected store

- Operation ID: `store_detail`
- Slug: `store-detail`
- Category: Stores
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-store-detail
- Markdown: https://eto.tools/dev/docs/stores/store-detail.md

Get details for a single connected store you own.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `shop_id` | path | `integer` | yes | Eto/Etsy shop id from /api/v1/stores |

#### Request

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

```python
import requests

url = "https://eto.tools/api/v1/stores/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/stores/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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-store-detail) shows the fields it returns.

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


## Research

Product research: demand signals and AI ranking for a keyword.

### POST /api/v1/product-hunt

Run product research (Product Hunt)

- Operation ID: `product_hunt`
- Slug: `product-hunt`
- Category: Research
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-product-hunt
- Markdown: https://eto.tools/dev/docs/research/product-hunt.md

Run the full Eto Product Hunt for a keyword — the exact same engine as the Product Hunt page: search Etsy -> POPULAR-NOW detection -> demand (units sold/24h) -> AI ranking.

THREE modes:
  1. SEARCH = THE DEFAULT. Use this ANY time the user names a topic to research ("research t-shirts, 100 products, optimized"). Pass ONLY "keyword" + "amount" (+ optional "max_listing_age", "optimized", "full_details"). Eto does the whole pipeline and returns ranked products with real demand.
  2. "listing_ids": ONLY when you ALREADY have specific Etsy listing ids to score.
  3. "listings": ONLY when you already have full listing objects (Eto just adds demand).

CRITICAL: for a topic search, do NOT first call listings_search and then pass its listing_ids here. That bypasses popular-now detection and demand comes back EMPTY (null). Just call SEARCH mode with the keyword and let Eto do it.

demand_24h (units sold in last 24h) is the popularity signal — only populated via the real pipeline (SEARCH mode, or popular listings). With "optimized" true, results are AI-ranked by opportunity and carry ai_score + ai_reason; otherwise sorted by demand. Send native JSON types (optimized true, not "true"); stringified values are tolerated. Note: Etsy calls may be billed against your plan usage.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `keyword` | body | `string` | yes | Search term / keyword (max 200 chars). |
| `amount` | body | `integer` | no | SEARCH mode: how many products to fetch & rank (1-8000, default 50). Large amounts take longer — demand extraction is heavy. |
| `max_listing_age` | body | `integer` | no | SEARCH mode: only listings created within the last N months (0 or omit = no limit, max 60). |
| `optimized` | body | `boolean` | no | SEARCH mode: AI-powered filtering & ranking. When true, results carry ai_score + ai_reason and full_details is ignored. |
| `full_details` | body | `boolean` | no | SEARCH mode (only when optimized is false): true returns full per-listing details; false returns listing_id + demand only (lightest). |
| `listing_ids` | body | `array` | no | Array of Etsy listing ids (integers). Use instead of a keyword search. |
| `listings` | body | `array` | no | Array of listing objects you already have. Eto just adds demand. |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/product-hunt" \
  -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "keyword": "leather wallet"
}'
```

```python
import requests

url = "https://eto.tools/api/v1/product-hunt"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "keyword": "leather wallet"
}

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/product-hunt";
const payload = {
  "keyword": "leather wallet"
};

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-product-hunt) shows the fields it returns.

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


## AI Studio

Generate listing titles, descriptions and images, and analyse product photos.

### POST /api/v1/ai/chat

AI chat

- Operation ID: `ai_chat`
- Slug: `ai-chat`
- Category: AI Studio
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-ai-chat
- Markdown: https://eto.tools/dev/docs/ai-studio/ai-chat.md

Free-form AI chat using your configured Gemini credentials. Requires AI credentials configured in Eto settings.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `message` | body | `string` | yes | The user message / prompt |
| `context` | body | `object` | no | Optional context object merged into the prompt |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/ai/chat" \
  -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "message": "Suggest three tag ideas for a leather wallet listing."
}'
```

```python
import requests

url = "https://eto.tools/api/v1/ai/chat"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "message": "Suggest three tag ideas for a leather wallet listing."
}

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/ai/chat";
const payload = {
  "message": "Suggest three tag ideas for a leather wallet listing."
};

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-ai-chat) shows the fields it returns.

#### 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/ai/generate-title

AI: generate a listing title

- Operation ID: `ai_generate_title`
- Slug: `ai-generate-title`
- Category: AI Studio
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-ai-generate-title
- Markdown: https://eto.tools/dev/docs/ai-studio/ai-generate-title.md

Generate an Etsy listing title for a keyword.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `keyword` | body | `string` | yes | Target keyword |
| `product_description` | body | `string` | no | Optional product description for context |
| `style` | body | `string` | no | Tone/style (default "professional") |
| `max_length` | body | `integer` | no | Max title length (capped at 200, default 140) |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/ai/generate-title" \
  -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "keyword": "leather wallet"
}'
```

```python
import requests

url = "https://eto.tools/api/v1/ai/generate-title"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "keyword": "leather wallet"
}

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/ai/generate-title";
const payload = {
  "keyword": "leather wallet"
};

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-ai-generate-title) shows the fields it returns.

#### 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/ai/generate-description

AI: generate a listing description

- Operation ID: `ai_generate_description`
- Slug: `ai-generate-description`
- Category: AI Studio
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-ai-generate-description
- Markdown: https://eto.tools/dev/docs/ai-studio/ai-generate-description.md

Generate an Etsy listing description. Provide at least one of "keyword" or "title".

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `keyword` | body | `string` | no | Target keyword (keyword or title required) |
| `title` | body | `string` | no | Listing title (keyword or title required) |
| `features` | body | `array` | no | Array of product feature strings |
| `tone` | body | `string` | no | Tone (default "professional") |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/ai/generate-description" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

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

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

```javascript
const url = "https://eto.tools/api/v1/ai/generate-description";

const response = await fetch(url, {
  method: "POST",
  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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-ai-generate-description) shows the fields it returns.

#### 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/ai/generate-image

AI: generate an image (async)

- Operation ID: `ai_generate_image`
- Slug: `ai-generate-image`
- Category: AI Studio
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-ai-generate-image
- Markdown: https://eto.tools/dev/docs/ai-studio/ai-generate-image.md

Start an AI image generation job from a text prompt. Returns a job_id; poll ai_generate_image_status for the result.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `prompt` | body | `string` | yes | Image prompt |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/ai/generate-image" \
  -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "prompt": "A leather wallet on a walnut desk, soft daylight"
}'
```

```python
import requests

url = "https://eto.tools/api/v1/ai/generate-image"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "prompt": "A leather wallet on a walnut desk, soft daylight"
}

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/ai/generate-image";
const payload = {
  "prompt": "A leather wallet on a walnut desk, soft daylight"
};

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-ai-generate-image) shows the fields it returns.

#### 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/ai/generate-image/status

AI: image generation status

- Operation ID: `ai_generate_image_status`
- Slug: `ai-generate-image-status`
- Category: AI Studio
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-ai-generate-image-status
- Markdown: https://eto.tools/dev/docs/ai-studio/ai-generate-image-status.md

Poll the status/result of an AI image generation job.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `job_id` | query | `string` | yes | Job id returned by ai_generate_image |

#### Request

```bash
curl -X GET "https://eto.tools/api/v1/ai/generate-image/status?job_id=job_9f2c41d8" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/ai/generate-image/status"
headers = {"X-Eto-API-Key": "eto_your_key"}
params = {
    "job_id": "job_9f2c41d8"
}

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

```javascript
const url = "https://eto.tools/api/v1/ai/generate-image/status?job_id=job_9f2c41d8";

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-ai-generate-image-status) shows the fields it returns.

#### 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/ai/analyze-image

AI: analyze a product image

- Operation ID: `ai_analyze_image`
- Slug: `ai-analyze-image`
- Category: AI Studio
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-ai-analyze-image
- Markdown: https://eto.tools/dev/docs/ai-studio/ai-analyze-image.md

Analyze a product image and return a description suitable for an Etsy listing.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `image` | body | `object` | yes | Object with "data" (base64) and "mimeType" (e.g. image/png) |
| `prompt` | body | `string` | no | Optional instruction (default describes the product image) |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/ai/analyze-image" \
  -H "X-Eto-API-Key: eto_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "image": {}
}'
```

```python
import requests

url = "https://eto.tools/api/v1/ai/analyze-image"
headers = {"X-Eto-API-Key": "eto_your_key"}
payload = {
    "image": {}
}

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/ai/analyze-image";
const payload = {
  "image": {}
};

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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-ai-analyze-image) shows the fields it returns.

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


## Webhooks

Inspect and re-send the order events Eto pushes to your server.

### GET /api/v1/webhooks/deliveries

List webhook deliveries

- Operation ID: `webhooks_deliveries`
- Slug: `webhooks-deliveries`
- Category: Webhooks
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-webhooks-deliveries
- Markdown: https://eto.tools/dev/docs/webhooks/webhooks-deliveries.md

List recent outbound webhook deliveries from your Eto account, newest first. Pass `id` to fetch one delivery on its own, which is the only form that includes the full payload Eto sent. Verified against developer_api/webhook_views.py.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | query | `integer` | no | Fetch this one delivery instead of a page, including its full payload. Ids come from the `deliveries[].id` field of a list call. |
| `status` | query | `string` | no | Only deliveries in this state. Any other value is ignored. One of: `pending`, `success`, `failed`. |
| `limit` | query | `integer` | no | Page size. Values above 100 are clamped. Default: `25`. Constraints: max 100. |
| `offset` | query | `integer` | no | Pagination offset. Default: `0`. |

#### Request

```bash
curl -X GET "https://eto.tools/api/v1/webhooks/deliveries?id=1&status=pending" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/webhooks/deliveries"
headers = {"X-Eto-API-Key": "eto_your_key"}
params = {
    "id": 1,
    "status": "pending"
}

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

```javascript
const url = "https://eto.tools/api/v1/webhooks/deliveries?id=1&status=pending";

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

```json
{
  "success": true,
  "total": 128,
  "limit": 25,
  "offset": 0,
  "deliveries": [
    {"id": 8842, "event_type": "order.paid", "status": "success", "response_status_code": 200}
  ]
}
```

#### 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/webhooks/deliveries/{delivery_id}/retry

Re-send one webhook delivery

- Operation ID: `webhook_delivery_retry`
- Slug: `webhook-delivery-retry`
- Category: Webhooks
- Authentication: API key only. Reads or writes data Eto holds for your own account.
- HTML: https://eto.tools/dev/docs/#ep-webhook-delivery-retry
- Markdown: https://eto.tools/dev/docs/webhooks/webhook-delivery-retry.md

Re-deliver exactly one past webhook delivery — the single delivery_id you pass — to your currently configured URL, and return the new outcome (success, response_status_code, error). It never re-sends anything else. Find delivery ids with GET /api/v1/webhooks/deliveries.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `delivery_id` | path | `integer` | yes | Id of the delivery to re-send, from GET /api/v1/webhooks/deliveries. |

#### Request

```bash
curl -X POST "https://eto.tools/api/v1/webhooks/deliveries/8842/retry" \
  -H "X-Eto-API-Key: eto_your_key"
```

```python
import requests

url = "https://eto.tools/api/v1/webhooks/deliveries/8842/retry"
headers = {"X-Eto-API-Key": "eto_your_key"}

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

```javascript
const url = "https://eto.tools/api/v1/webhooks/deliveries/8842/retry";

const response = await fetch(url, {
  method: "POST",
  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`. This endpoint has no worked response example in the registry yet; the [HTML reference](https://eto.tools/dev/docs/#ep-webhook-delivery-retry) shows the fields it returns.

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


## Where everything is

Every page of the Eto app, every section of every page, and every feature with the exact path to reach it. Paths read left to right from the page, so "Listing Manager > Edit Storewide > Auto Quantity Control" means: open Listing Manager, switch to Edit Storewide, and the control is in there. 1104 features across 16 pages. The same map is available as JSON at https://eto.tools/dev/docs/feature-map.json.

### Pages

- [Dashboard](https://eto.tools/dashboard/): The account home page: a store header, four tabs, a live order feed, a money statement with a chart, and boards for out-of-stock listings and listing performance. (84 features)
- [Analytics](https://eto.tools/dashboard/analytics/): A ten-tab reporting page over orders, money, stores, listings and operations, with a KPI strip, a live event stream and an orders map. (137 features)
- [Orders](https://eto.tools/dashboard/orders/): One list of every Etsy order across your connected stores, with costs, tracking, notes and supplier sharing. (86 features)
- [Finance](https://eto.tools/dashboard/finances/): Your Etsy money by day or date range: balance, payouts, sales, fees, ads, refunds, COGS and profit, with the ledger behind every figure. (69 features)
- [Stores](https://eto.tools/dashboard/stores/): Connect Etsy shops to Eto and control what each one shows on the rest of the app. (45 features)
- [Listing Manager](https://eto.tools/dashboard/listings/): Browse, filter and bulk edit every listing across your connected Etsy shops. (74 features)
- [Quick upload](https://eto.tools/dashboard/single-research/): Build Etsy listings in Eto and push them to one or more connected stores as drafts. (104 features)
- [VA Admin](https://eto.tools/dashboard/va-admin/): Create virtual assistant logins, set what each one may do per store, and review what they have done. (40 features)
- [Product Hunt](https://eto.tools/dashboard/etsy-search/): Searches the Etsy marketplace for a keyword, scores every listing for 24h demand, and shows the results as a filterable card grid. (39 features)
- [Shop Spy](https://eto.tools/dashboard/shop-spy/): Takes any Etsy listing link and pulls that whole shop, then lets you sort and filter every active listing it has. (20 features)
- [Gallery](https://eto.tools/dashboard/gallery/): Chat with Gemini to generate, edit and keep product imagery for your listings. (34 features)
- [Workflow](https://eto.tools/dashboard/workflow/): Puts Product Hunt, AI Studio and Quick Upload side by side in three resizable panes so a product can go from research to published without changing page. (29 features)
- [Developer API console](https://eto.tools/dashboard/api/): Where an Enterprise account creates its API key, connects an MCP client, and configures and watches outbound webhooks. (29 features)
- [Settings](https://eto.tools/settings/): Seven tabs holding account, billing, AI provider, notification, order integration, theme and affiliate settings. (46 features)
- [Sidebar](https://eto.tools/dashboard/): The rail down the left of every dashboard page: navigation, the theme switch, and the quick panels that float research tools over whatever page you are on. (38 features)
- [Command palette](https://eto.tools/dashboard/): Cmd+K from anywhere: one box that jumps to a page, runs a control on the page you are on, or searches your listings, orders and stores. (230 features)

### Dashboard

- URL: https://eto.tools/dashboard/
- Anchor: https://eto.tools/dev/docs/#where-dashboard
- Markdown: https://eto.tools/dev/docs/where/dashboard.md
- Features: 84

The account home page: a store header, four tabs, a live order feed, a money statement with a chart, and boards for out-of-stock listings and listing performance.

#### Store header

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Store switcher | Opens a menu to pick which connected store the whole page reports on. | `Dashboard > store name` (needs connected store) | - |
| Switch store | The menu heading over the store list, with the line "Pick a store to load its numbers". | `Dashboard > store name` (needs connected store) | - |
| Search stores | Filters the store menu by name as you type. | `Dashboard > store name > Search stores` (needs connected store) | - |
| Most sales | Sorts the store menu by lifetime sales, highest first. | `Dashboard > store name > Most sales` (needs connected store) | - |
| Least sales | Sorts the store menu by lifetime sales, lowest first. | `Dashboard > store name > Most sales > Least sales` (needs connected store) | - |
| A to Z | Sorts the store menu alphabetically by shop name. | `Dashboard > store name > Most sales > A to Z` (needs connected store) | - |
| Store stats line | Shows the selected store lifetime sales, its active listing count and a link out to the shop on Etsy. | `Dashboard > store header` (needs connected store) | - |
| Refresh indicator | A spinner and label in the stats line saying that the store numbers are being re-synced right now. | `Dashboard > store header` (needs connected store) | - |
| All stores | Switches the page to a combined view of every connected store instead of one. | `Dashboard > All stores` (needs two or more stores) | - |
| All stores currency settings | Opens the setup modal for the common currency and the per-store conversion rates. | `Dashboard > All stores currency settings` (needs two or more stores) | - |
| [Connect a store](https://eto.tools/dashboard/stores/) | The empty state shown when no store is connected, linking to the Stores page. | `Dashboard > Connect a store` | - |

#### All Stores Setup modal

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Convert all to | Sets the three-letter currency every store is converted into for the combined view. | `Dashboard > All stores currency settings > Convert all to` (needs two or more stores) | - |
| Auto-fill rates | Fetches a conversion rate for each store currency into the common currency. | `Dashboard > All stores currency settings > Auto-fill rates` (needs two or more stores) | - |
| Per-store rate list | Lists every store with its own rate field, and notes that stores without a rate are left out. | `Dashboard > All stores currency settings` (needs two or more stores) | - |
| Save & View All Stores | Saves the currency and rates and switches the page into the combined all-stores view. | `Dashboard > All stores currency settings > Save & View All Stores` (needs two or more stores) | - |
| Cancel | Closes the setup modal without saving the currency or rates. | `Dashboard > All stores currency settings > Cancel` (needs two or more stores) | - |

#### Tabs

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Home | The default tab: live updates, the money statement and chart, recent orders and latest listings. | `Dashboard > Home` | - |
| Issues | The tab that checks every connected store for broken or expired connections. | `Dashboard > Issues` | - |
| Listings | The tab listing every active listing sitting at zero stock, with a count badge on the tab itself. | `Dashboard > Listings` | - |
| Performance | The tab showing views, favourites and orders per listing over time from the capture history. | `Dashboard > Performance` | - |

#### Home tab: Live updates

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| This store only | A toggle that limits the live cards to the store picked in the header. | `Dashboard > Home > Live updates > This store only` | - |
| Order events | A live-streamed list of order events as they arrive, with a connection dot and a running count. | `Dashboard > Home > Order events` | - |
| Clear | Removes every event currently listed in the Order events card. | `Dashboard > Home > Order events > Clear` | - |
| Needs fulfillment | Lists paid orders that have not shipped yet, and says "Every paid order is on its way" when there are none. | `Dashboard > Home > Needs fulfillment` | - |
| Inactive stores | A toggle that also lists unshipped orders from banned, disconnected or deleted stores. | `Dashboard > Home > Needs fulfillment > Inactive stores` | - |
| [View All](https://eto.tools/dashboard/orders/?shipped=false&is_digital=false) | Opens the Orders page filtered to unshipped physical orders. | `Dashboard > Home > Needs fulfillment > View All` | - |

#### Home tab: money period and view controls

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Today | Reports the statement, the hero figure and the chart for today only. | `Dashboard > Home > Today` | - |
| Yesterday | Reports the money block for yesterday. | `Dashboard > Home > Yesterday` | - |
| Last 7 days | Reports the money block for the last seven days. | `Dashboard > Home > Last 7 days` | - |
| This Month | Reports the money block for the current calendar month. | `Dashboard > Home > This Month` | - |
| This Year | Reports the money block for the current calendar year. | `Dashboard > Home > This Year` | - |
| Custom | Opens a single-month calendar to pick one day or a date range for the money block. | `Dashboard > Home > Custom` | - |
| Calendar Apply | Applies the day or range picked in the custom calendar. | `Dashboard > Home > Custom > Apply` | - |
| Calendar Clear | Clears the custom date selection and returns to the preset periods. | `Dashboard > Home > Custom > Clear` | - |
| Settled | Shows money on the day it actually moved in the Etsy ledger. | `Dashboard > Home > Settled` | - |
| Outcome | Groups every order by its sale day and pulls later fees and refunds back onto that day. | `Dashboard > Home > Outcome` | - |
| Settled and Outcome help | A hover card that explains the difference between the Settled and Outcome lenses. | `Dashboard > Home > Settled` | - |
| Orders | Plots order counts on the dashboard chart. | `Dashboard > Home > Orders` | - |
| Sales | Plots sales money on the dashboard chart. | `Dashboard > Home > Sales` | - |
| COGS | Plots cost of goods on the dashboard chart. | `Dashboard > Home > COGS` | - |

#### Home tab: Statement

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Statement | The money block subtitled "Money in, fees out", running from gross sales down to net profit. | `Dashboard > Home > Statement` | - |
| Gross sales | The total money customers paid in the chosen period. | `Dashboard > Home > Statement` | - |
| Refunds | The money refunded to customers in the chosen period. | `Dashboard > Home > Statement` | - |
| Fees | The Etsy fees charged in the chosen period. | `Dashboard > Home > Statement` | - |
| Ads | The advertising spend in the chosen period. | `Dashboard > Home > Statement` | - |
| Software | The software costs subtracted in the chosen period. | `Dashboard > Home > Statement` | - |
| Gross profit | Gross sales minus refunds, fees, ads and software. | `Dashboard > Home > Statement` | - |
| COGS row | The cost of goods subtracted from gross profit. | `Dashboard > Home > Statement` | - |
| Net profit | The bottom line after cost of goods is taken off gross profit. | `Dashboard > Home > Statement` | - |
| Statement footer | A quiet line with the sale count, the average order value, net sales and the amount paid out. | `Dashboard > Home > Statement` | - |
| [Open Finance page](https://eto.tools/dashboard/finances/) | Leaves the dashboard for the full finance breakdown. | `Dashboard > Home > Statement > Open Finance page` | - |

#### Home tab: chart and overlays

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Net profit hero | The large net profit figure for the chosen window, with the change against the previous one. | `Dashboard > Home` | - |
| Verification line | A small line under the hero saying whether the figures were checked against Etsy, and which stores need reconnecting. | `Dashboard > Home` | - |
| Orders chart | The main dashboard chart drawing the chosen measure across the chosen period. | `Dashboard > Home` | - |
| Connect a store overlay | Covers the money block until at least one store is connected, with a link to the Stores page. | `Dashboard > Home` | - |

#### Home tab: recent activity

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Recent orders | The last orders across every store, each row opening that order on the Orders page. | `Dashboard > Home > Recent orders` | - |
| [All orders](https://eto.tools/dashboard/orders/) | Opens the Orders page from the Recent orders card. | `Dashboard > Home > Recent orders > All orders` | - |
| Latest listings | The most recently synced listings with price, views, favourites and state. | `Dashboard > Home > Latest listings` | - |
| [Listing manager](https://eto.tools/dashboard/listings/) | Opens the Listing Manager from the Latest listings card. | `Dashboard > Home > Latest listings > Listing manager` | - |

#### Issues tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Connection check | Walks every connected store one at a time and reports whether its Etsy connection still works. | `Dashboard > Issues > Connection check` (needs connected store) | - |
| Check | Starts the connection check and turns into Stop while it runs. | `Dashboard > Issues > Check` (needs connected store) | - |
| Check progress bar | A bar and an "n / total" counter showing how far through the stores the check is. | `Dashboard > Issues > Check` (needs connected store) | - |
| Needs attention | Lists the stores that failed the check with the reason and a link to reconnect them. | `Dashboard > Issues > Needs attention` (needs connected store) | - |

#### Listings tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Out of stock | A per-store board of every active listing sitting at zero stock, with an estimate of the money the empty shelf has cost. | `Dashboard > Listings` (needs connected store) | - |
| Search a listing title | Filters the out-of-stock board to listings whose title matches. | `Dashboard > Listings > Search a listing title` (needs connected store) | - |
| 90 days | Prices each out-of-stock listing by what it earned in the last ninety days. | `Dashboard > Listings > 90 days` (needs connected store) | - |
| Lifetime | Prices each out-of-stock listing by what it has earned for all time. | `Dashboard > Listings > Lifetime` (needs connected store) | - |
| Sort the stores | Orders the store groups by out of stock count, revenue, lifetime sales, listing count, store age or name. | `Dashboard > Listings > Sort the stores` (needs two or more stores) | - |
| Flip the sort order | Reverses the store ordering between highest first and lowest first. | `Dashboard > Listings > Flip the sort order` (needs two or more stores) | - |
| Show more | Appends the next page of out-of-stock rows inside one store group without reloading the board. | `Dashboard > Listings > Show more` (needs connected store) | - |
| [Restock](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Opens that listing in the editor with the quantity field already in view. | `Dashboard > Listings > Restock` (needs connected store) | - |
| Etsy link | Opens the out-of-stock listing on Etsy in a new tab. | `Dashboard > Listings > Etsy` (needs connected store) | - |

#### Performance tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Listing performance | Views, favourites, listing counts and orders read off the six-hourly capture history. | `Dashboard > Performance` (needs connected store) | - |
| 24 hours | Reports listing performance over the last day. | `Dashboard > Performance > 24 hours` (needs connected store) | - |
| 7 days | Reports listing performance over the last week. | `Dashboard > Performance > 7 days` (needs connected store) | - |
| 30 days | Reports listing performance over the last month. | `Dashboard > Performance > 30 days` (needs connected store) | - |
| Capture now | Records every listing on every store immediately, at most once every six hours. | `Dashboard > Performance > Capture now` (needs connected store) | - |
| Figure strip | Five counters across the top: Views, Favourites, Listings, Sold out and Orders. | `Dashboard > Performance` (needs connected store) | - |
| Capture status line | A mono line saying what the running capture is doing, or when the last one ran and what it cost. | `Dashboard > Performance` (needs connected store) | - |
| Views and favourites over time | A chart of the capture series, hidden until there are at least two captures. | `Dashboard > Performance` (needs connected store) | - |
| Movement | Plots views gained, favourites gained and orders per capture. | `Dashboard > Performance > Movement` (needs connected store) | - |
| Totals | Plots the running total of views and favourites instead of the movement between captures. | `Dashboard > Performance > Totals` (needs connected store) | - |
| Chart legend | Clickable series names that switch each line on the performance chart on or off. | `Dashboard > Performance` (needs connected store) | - |
| Per-store table | One row per store with active, sold out, views, favourites, orders, a views sparkline and the last capture time. | `Dashboard > Performance` (needs connected store) | - |

### Analytics

- URL: https://eto.tools/dashboard/analytics/
- Anchor: https://eto.tools/dev/docs/#where-analytics
- Markdown: https://eto.tools/dev/docs/where/analytics.md
- Features: 137

A ten-tab reporting page over orders, money, stores, listings and operations, with a KPI strip, a live event stream and an orders map.

#### Page header

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Store switcher | Picks one connected store or All stores for every panel on the page. | `Analytics > All stores` | - |
| Search stores | Filters the store switcher list by shop name, with a match count beside it. | `Analytics > All stores > Search stores` | - |
| Today | Reports every panel over today only. | `Analytics > Today` | - |
| 7d | Reports every panel over the last seven days. | `Analytics > 7d` | - |
| 30d | Reports every panel over the last thirty days, the page default. | `Analytics > 30d` | - |
| 90d | Reports every panel over the last ninety days. | `Analytics > 90d` | - |
| YTD | Reports every panel from the start of the year to today. | `Analytics > YTD` | - |
| Custom | Reveals a start date, an end date and an Apply button for an arbitrary window. | `Analytics > Custom` | - |
| Compare against | Chooses what every KPI delta is measured against: the previous period, the same period last year, or nothing. | `Analytics > vs previous period` | - |
| Live indicator | Shows whether the page is connected to the live event stream, reconnecting, or paused. | `Analytics > Live` | - |
| Refresh | Throws away the cached answers and re-fetches every panel for the current store and period. | `Analytics > Refresh` | - |

#### KPI strip

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Orders | The order count for the period, with its change against the comparison window. | `Analytics > KPI strip` | - |
| Revenue | Gross money taken in the period. | `Analytics > KPI strip` | - |
| Net profit | Revenue less fees, ads, refunds and cost of goods. | `Analytics > KPI strip` | - |
| COGS | The cost of goods for the orders in the period. | `Analytics > KPI strip` | - |
| Etsy fees | Every Etsy fee charged in the period. | `Analytics > KPI strip` | - |
| Ads | Advertising spend in the period. | `Analytics > KPI strip` | - |
| Average order | Revenue divided by the number of orders. | `Analytics > KPI strip` | - |
| Refund rate | The share of orders that were refunded, coloured so a rise reads as bad. | `Analytics > KPI strip` | - |
| Active listings | How many listings are active right now across the stores in view. | `Analytics > KPI strip` | - |
| Comparison caption | The single line under the strip naming the baseline window every delta is measured against. | `Analytics > KPI strip` | - |

#### Overview tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Orders and money | The headline chart plotting orders, revenue and gross profit across the period. | `Analytics > Overview > Orders and money` | - |
| Both / Orders / Money | Switches the headline chart between both axes, order counts only, or money only. | `Analytics > Overview > Orders and money > Both` | - |
| This period | Total revenue and order count for the window, plus average order, days with ledger data, stores in view and order cost coverage. | `Analytics > Overview > This period` | - |
| Where the money goes | A ranked bar list of revenue, net profit, COGS, fees and ads. | `Analytics > Overview > Where the money goes` | - |
| Rates | Refund rate, fees, ads and COGS as shares of revenue, and the active listing count. | `Analytics > Overview > Rates` | - |
| Live updates | The event feed, prepending new orders and order events as the stream delivers them. | `Analytics > Overview > Live updates` | - |

#### Orders tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Order volume | Orders, shipped orders and revenue across the period. | `Analytics > Orders > Order volume` | - |
| Count / Money | Switches the volume chart between order counts and revenue. | `Analytics > Orders > Order volume > Count` | - |
| Top products | The best selling products drawn as a ranked chart, with its own order-by menu. | `Analytics > Orders > Top products` | - |
| Needs fulfilment | How many paid orders are waiting to ship, with the oldest of them listed and linked. | `Analytics > Orders > Needs fulfilment` | - |
| By status | Order counts split by Paid, Completed, Open, Processing, Cancelled and Unknown. | `Analytics > Orders > By status` | - |
| By country | Order counts split by buyer country. | `Analytics > Orders > By country` | - |
| By store | Order counts split by shop. | `Analytics > Orders > By store` | - |
| Fulfilment | Average time to ship, lines shipped, orders shipped, shipped share and gift orders. | `Analytics > Orders > Fulfilment` | - |
| Money per order | Revenue, Etsy fees, COGS, net profit and refunded orders for the window. | `Analytics > Orders > Money per order` | - |
| Top products, in figures | The same products as a table of product, listing id, units and order lines. | `Analytics > Orders > Top products, in figures` | - |

#### Map tab: toolbar

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Size by | Chooses whether places on the map are sized by Orders, Order value, Items or Average order. | `Analytics > Map > Size by` (needs placed orders) | - |
| Dots | Draws each place as a clustered dot. | `Analytics > Map > Dots` (needs placed orders) | - |
| Heat | Draws the orders as a heat surface instead of dots. | `Analytics > Map > Heat` (needs placed orders) | - |
| Countries | Fills whole countries by their share instead of plotting places. | `Analytics > Map > Countries` (needs placed orders) | - |
| Flat | Draws the map as a flat projection. | `Analytics > Map > Flat` (needs placed orders) | - |
| 3D | Draws the map as a globe, with a starfield behind it. | `Analytics > Map > 3D` (needs placed orders) | - |
| Find a place | Searches the places currently on the map and flies to the one you pick. | `Analytics > Map > Find a place` (needs placed orders) | - |
| Reload the map | Re-fetches the map points, spinning while anything is in flight. | `Analytics > Map > Reload the map` (needs placed orders) | - |
| Fit the map to your orders | Zooms and pans so every placed order is on screen. | `Analytics > Map > Fit the map to your orders` (needs placed orders) | - |
| Back to the whole world | Resets the camera to the default world view. | `Analytics > Map > Back to the whole world` (needs placed orders) | - |
| Download these places as CSV | Saves the places on screen with orders, items, value, average order, share and coordinates as a CSV file. | `Analytics > Map > Download these places as CSV` (needs placed orders) | - |
| Show or hide the side panel | Collapses or reopens the details panel beside the map. | `Analytics > Map > Show or hide the side panel` (needs placed orders) | - |
| Fill the window | Expands the map to fill the browser window. | `Analytics > Map > Fill the window` (needs placed orders) | - |
| Zoom in | Steps the map camera one level closer. | `Analytics > Map > Zoom in` (needs placed orders) | - |
| Zoom out | Steps the map camera one level back. | `Analytics > Map > Zoom out` (needs placed orders) | - |
| Place names | Turns the place-name labels on the map on or off. | `Analytics > Map > Place names` (needs placed orders) | - |
| Resize the panel | A drag handle, also movable with the arrow keys, that sets how wide the side panel is. | `Analytics > Map > Resize the panel` (needs placed orders) | - |
| Place my orders on the map | Works out where past orders came from, offline from the addresses already stored, so the map has something to draw. | `Analytics > Map > Place my orders on the map` (needs account owner) | - |

#### Map tab: side panel

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Overview | The panel tab counting the places reached, with the Reach, Top countries and Top places sections. | `Analytics > Map > Overview` (needs placed orders) | - |
| Reach | Countries, new places this period, orders, items, order value, average order and how many orders were placed to a town. | `Analytics > Map > Overview > Reach` (needs placed orders) | - |
| Top countries | The countries with the most orders, each with how many distinct places it covers. | `Analytics > Map > Overview > Top countries` (needs placed orders) | - |
| Top places | The individual towns and districts with the most orders. | `Analytics > Map > Overview > Top places` (needs placed orders) | - |
| Place | The panel tab showing one place picked on the map in full. | `Analytics > Map > Place` (needs placed orders) | - |
| This period | Order value, average order, growth against the previous period, buyers, repeat buyers, first and last order, and how precisely the place was located. | `Analytics > Map > Place > This period` (needs placed orders) | - |
| What it buys | The products that place buys, with each product share and its lift against the whole account. | `Analytics > Map > Place > What it buys` (needs placed orders) | - |
| Stores selling here | Which of your shops have sold into that place, and how many orders each. | `Analytics > Map > Place > Stores selling here` (needs placed orders) | - |
| Insights | The panel tab ranking places into five lists with a Show button that flies the map to each. | `Analytics > Map > Insights` (needs placed orders) | - |
| Biggest markets | The places bringing in the most money. | `Analytics > Map > Insights > Biggest markets` (needs placed orders) | - |
| Growing fastest | The places whose order count rose most against the period before. | `Analytics > Map > Insights > Growing fastest` (needs placed orders) | - |
| Biggest baskets | The places with the highest average order value. | `Analytics > Map > Insights > Biggest baskets` (needs placed orders) | - |
| Barely served | Places that spend well but that only one of your stores, or none, reaches. | `Analytics > Map > Insights > Barely served` (needs placed orders) | - |
| Gone quiet | Places that ordered last period and nothing this one. | `Analytics > Map > Insights > Gone quiet` (needs placed orders) | - |
| What each country buys | A per-country product table added under the rankings, with each product lift for that country. | `Analytics > Map > Insights` (needs placed orders) | - |
| Live | The panel tab counting orders in the last minute and listing each new one as it lands and pulses on the map. | `Analytics > Map > Live` (needs placed orders) | - |

#### Finance tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Revenue, profit and cost | The headline money chart plotting revenue, gross profit, COGS and net profit, with fees, ads and refunds available behind the legend. | `Analytics > Finance > Revenue, profit and cost` | - |
| Lines / Costs | Switches the money chart between the profit lines and the cost lines. | `Analytics > Finance > Revenue, profit and cost > Lines` | - |
| Money per store | Every store ranked as a chart, ordered by revenue, gross profit, fees, ads, sales or name. | `Analytics > Finance > Money per store` | - |
| Totals | Net profit as a hero figure over revenue, gross profit, COGS, Etsy fees, ads, refunds, cancelled and paid out to bank. | `Analytics > Finance > Totals` | - |
| Fees breakdown | Each kind of Etsy fee as its own bar. | `Analytics > Finance > Fees breakdown` | - |
| Ads breakdown | Each kind of advertising charge as its own bar. | `Analytics > Finance > Ads breakdown` | - |
| Taxes and other | Every remaining ledger movement that is not a fee or an ad. | `Analytics > Finance > Taxes and other` | - |
| Per store | A sortable, paged table of store, sales, revenue, fees, ads, refunds and gross profit. | `Analytics > Finance > Per store` | - |

#### Stores tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Store comparison | Plots every store on two axes you choose, or reads the same stores as a ranked league table. | `Analytics > Stores > Store comparison` | - |
| X axis picker | Chooses which measure the comparison plot uses for its horizontal axis. | `Analytics > Stores > Store comparison > X` | - |
| Y axis picker | Chooses which measure the comparison plot uses for its vertical axis. | `Analytics > Stores > Store comparison > Y` | - |
| Top 10 / 25 / 50 / All stores | Limits the comparison plot to the leading stores by the Y measure, or shows every store. | `Analytics > Stores > Store comparison > Top 25` | - |
| Revenue leaders | A fixed-height ranked list of shops by revenue for the period. | `Analytics > Stores > Revenue leaders` | - |
| Flags | The stores that need reconnecting, are on vacation, or are hidden from a page, each with a chip saying which. | `Analytics > Stores > Flags` | - |
| Store age against sales | Plots how old each store is against its lifetime sales. | `Analytics > Stores > Store age against sales` | - |
| Every store | A sortable, paged table of store, products, active, stock, views, favourites, orders, revenue, profit, lifetime sales and age. | `Analytics > Stores > Every store` | - |

#### Listings tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Revenue per listing | Every listing ranked by what it earned, windowed so thousands of bars stay readable. | `Analytics > Listings > Revenue per listing` | - |
| Find a listing | Jumps the revenue ranking to the listing whose title or id matches. | `Analytics > Listings > Revenue per listing > Find a listing` | - |
| Revenue / Profit / Units / Orders | Chooses which measure the revenue-per-listing bars are drawn from. | `Analytics > Listings > Revenue per listing > Revenue` | - |
| Show 50 / 100 / 200 / 400 | Sets how many listings are on screen in one window of the ranking. | `Analytics > Listings > Revenue per listing > Show 50` | - |
| Currency picker | Appears only when the stores in view price in more than one currency, and picks which one the ranking is drawn in. | `Analytics > Listings > Revenue per listing` (needs two or more currencies) | - |
| Ranking brush | A slider under the chart, also movable with the arrow keys, that scrolls the window along the full ranking. | `Analytics > Listings > Revenue per listing` | - |
| Zero-revenue band | The band at the end of the axis that opens a paged drawer of every listing that earned nothing in the period. | `Analytics > Listings > Revenue per listing` | - |
| Listings created | Plots listings created in each bucket against how many are now active and how many are still drafts. | `Analytics > Listings > Listings created` | - |
| Price spread | A histogram of what active listings are priced at. | `Analytics > Listings > Price spread` | - |
| Catalogue | Active listings as a hero figure over draft, inactive, expired, sold out, gone from Etsy, digital, with variations and units in stock. | `Analytics > Listings > Catalogue` | - |
| Low stock | Active listings nearly out of stock, each linked to its editor. | `Analytics > Listings > Low stock` | - |
| Engagement | Total views and favourites, favourites per hundred views, views per active listing and the number of listings tracked. | `Analytics > Listings > Engagement` | - |
| Most viewed | The listings with the most views, with their store, views and favourites. | `Analytics > Listings > Most viewed` | - |
| Least viewed | The listings with the fewest views, with their store, price and views. | `Analytics > Listings > Least viewed` | - |

#### Listing stats tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Performance over time | The capture series for views, favourites and orders across the chosen window. | `Analytics > Listing stats > Performance over time` | - |
| Store picker | Narrows the performance chart to one store that has been captured, or all of them. | `Analytics > Listing stats > Performance over time > All stores` | - |
| Movement / Lifetime totals / Stock and orders | Switches the performance chart between movement per capture, running totals, and stock against orders. | `Analytics > Listing stats > Performance over time > Movement` | - |
| What to do | Read-off suggestions from the movement, each row opening the listing it refers to. | `Analytics > Listing stats > What to do` | - |
| Biggest movers | The listings that gained and lost the most views in the window. | `Analytics > Listing stats > Biggest movers` | - |
| Most favourited | The listings that gained and lost the most favourites in the window. | `Analytics > Listing stats > Most favourited` | - |
| Gone quiet | Active listings nobody has looked at in a month. | `Analytics > Listing stats > Gone quiet` | - |
| Sold out, still wanted | Listings with no stock left whose favourites are still rising. | `Analytics > Listing stats > Sold out, still wanted` | - |
| One listing over time | The history of a single listing with views, favourites, stock and the orders that landed against it. | `Analytics > Listing stats > Every listing > a row` | - |
| Every listing | A sortable, paged table of listing, store, state, views gained, favourites gained, orders, conversion, stock change, views and favourites. | `Analytics > Listing stats > Every listing` | - |
| Search a title or id | Filters the Every listing table to matching listings. | `Analytics > Listing stats > Every listing > Search a title or id` | - |
| Capture log | Every capture sweep with its six-hour window, what started it, orders in the window, stores, listings, Etsy calls, duration and result. | `Analytics > Listing stats > Capture log` | - |

#### Suppliers tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Tracking submitted | Tracking entries suppliers have submitted through a share link, across the period. | `Analytics > Suppliers > Tracking submitted` | - |
| Supplier fulfilment | Orders marked done by suppliers, over orders shared, still open, with supplier tracking, active share links and link views. | `Analytics > Suppliers > Supplier fulfilment` | - |
| Tracking states | Tracking entries, how many name a carrier, how many have last-mile tracking, and how many of the period orders reached Etsy. | `Analytics > Suppliers > Tracking states` | - |
| Assignments | How many stores and products are assigned to a supplier, and the total share link count. | `Analytics > Suppliers > Assignments` | - |
| Open work by link | Each share link ranked by how many orders on it are still open. | `Analytics > Suppliers > Open work by link` | - |
| Share links | A table of every supplier link with its token, state, shared, done, open, views and last opened time. | `Analytics > Suppliers > Share links` | - |

#### VAs tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Assistant activity | Assistant actions across the period, covering logins, order actions and uploads. | `Analytics > VAs > Assistant activity` (needs account owner) | - |
| Team | How many assistants are active, and how many stores each of the first eight can reach. | `Analytics > VAs > Team` (needs account owner) | - |
| Actions per assistant | Action counts ranked by assistant. | `Analytics > VAs > Actions per assistant` (needs account owner) | - |
| What they did | Action counts ranked by the kind of action. | `Analytics > VAs > What they did` (needs account owner) | - |

#### API tab

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| API calls | Eto API and Etsy API traffic across the period, with error series behind the legend. | `Analytics > API > API calls` (needs account owner) | - |
| All / Errors | Switches the traffic chart between all calls and errors only. | `Analytics > API > API calls > All` (needs account owner) | - |
| Today's quota | Eto API calls left today over calls used, the daily limit, and the key prefix, state and last use. | `Analytics > API > Today's quota` (needs account owner) | - |
| Eto API | Calls, errors, error rate and average response time for the Eto API in the period. | `Analytics > API > Eto API` (needs account owner) | - |
| Etsy API | Calls, errors, error rate and average response time for Etsy in the period. | `Analytics > API > Etsy API` (needs account owner) | - |
| Research tools used | Counts of product hunt searches, bulk research runs, keyword searches, quick product and keyword lookups and shop spy searches. | `Analytics > API > Research tools used` (needs account owner) | - |
| Busiest Eto endpoints | A table of the Eto API endpoints with the most calls, their errors and their average response time. | `Analytics > API > Busiest Eto endpoints` (needs account owner) | - |
| Etsy calls by feature | A table of which Eto feature made the Etsy calls, with counts and errors. | `Analytics > API > Etsy calls by feature` (needs account owner) | - |

#### Controls every panel carries

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Line / Bar / Scatter | A per-chart switch that redraws that plot as a line, bars or a scatter, remembered for next visit. | `Analytics > any chart panel` | - |
| Order by | On a ranked chart, chooses which measure the ranking runs on, remembered per panel. | `Analytics > any ranked chart > Order` | - |
| Order direction | Flips a ranking between high to low, oldest first or A to Z depending on the measure. | `Analytics > any ranked chart` | - |
| Chart legend | Clickable series names that switch a line on the chart above them on or off. | `Analytics > any chart panel` | - |
| Column sorting | Clicking a table heading re-asks the server for that column, ordered high to low, then low to high, then cleared. | `Analytics > any table panel` | - |
| Table pager | Previous and next buttons that swap only the table panel, leaving the charts and the scroll position alone. | `Analytics > any table panel` | - |

### Orders

- URL: https://eto.tools/dashboard/orders/
- Anchor: https://eto.tools/dev/docs/#where-orders
- Markdown: https://eto.tools/dev/docs/where/orders.md
- Features: 86

One list of every Etsy order across your connected stores, with costs, tracking, notes and supplier sharing.

#### Header controls

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Store filter | Narrows the whole list to one store, and only appears once you have two or more connected. | `Orders > All Stores` (needs connected store) | - |
| Search anything | Searches orders by buyer name, email, phone, city, state, country, status, buyer message, gift message, shop name, product title or exact receipt number. | `Orders > Search anything...` (needs connected store) | `orders_list` |
| Clear search | Empties the search box and reloads the unsearched list. | `Orders > Search anything... > x` | - |
| Share | Opens the dialog that turns your current filters into a supplier link or a CSV. | `Orders > Share` (needs connected store) | - |
| Manage links | Opens the list of supplier links you have already created. | `Orders > (link icon beside Share)` | - |
| Sync Orders | Opens a date-range popover and then fetches fresh orders from Etsy for that window. | `Orders > Sync Orders` (needs connected store) | `orders_sync` |
| Sync date presets | Syncs the last 7, 30, 90, 180 or 365 days from Etsy in one click. | `Orders > Sync Orders > Last 30 days` (needs connected store) | `orders_sync` |
| Sync range calendar | Picks an exact start and end date to sync instead of a preset. | `Orders > Sync Orders > (calendar) > Sync range` (needs connected store) | `orders_sync` |
| No COGS | Toggles the list down to only the orders that have no cost of goods set. | `Orders > No COGS` | - |
| Sync Printify | Matches your orders to Printify orders and pulls their costs and tracking. | `Orders > Sync Printify` (needs Printify connected) | - |
| Import COGs | Opens the spreadsheet importer for bulk cost of goods entry. | `Orders > Import COGs` | - |

#### Filter bar

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Date Range | Limits the list to orders from the last 7, 30 or 60 days. | `Orders > Date Range` | - |
| Custom dates | Opens a calendar to pick an exact from and to date for the list. | `Orders > Custom dates` | - |
| Fulfilled | Shows only fulfilled or only unfulfilled orders. | `Orders > Fulfilled` | - |
| Shipped | Shows only physical orders that have shipped, or only those that have not. | `Orders > Shipped` | - |
| Paid | Shows only paid or only unpaid orders. | `Orders > Paid` | - |
| Location | Splits the list into US only or non-US orders. | `Orders > Location` | - |
| Message | Shows only orders that carry a buyer message, or only those that do not. | `Orders > Message` | - |
| Gift | Shows only orders the buyer marked as a gift, or only those they did not. | `Orders > Gift` | - |
| Type | Splits the list into physical, digital or multi-item orders. | `Orders > Type` | - |
| Urgency | Filters by time left to ship: overdue, 1 day left, 2 days left or 2+ days. | `Orders > Urgency` | - |
| Tracking | Filters by supplier tracking state: no tracking, awaiting tracking, tracking ready or fulfilled. | `Orders > Tracking` | - |
| Status | Filters by order state, including cancelled only, refunded only, payment processing only, and the matching hide options. | `Orders > Status` | - |
| Recently Refunded | Shows refunded orders ordered by when the refund was issued rather than when the order was placed. | `Orders > Status > Recently Refunded` | - |
| Fulfilled via | Shows only the orders fulfilled through Printify. | `Orders > Fulfilled via > Printify` | - |
| Clear | Drops every active filter at once and reloads the full list. | `Orders > Clear` | - |
| [Filter deep links](https://eto.tools/dashboard/orders/?order_status_filter=refunded_recent) | The page reads shipped, paid, us, message, gift, digital, shipped_physical, days_remaining, supplier_status, order_status_filter, fulfilled_via, date_from, date_to, days, shop_id and search or q from the URL so a filtered view can be linked to. | `Orders > (URL query string)` | `orders_list` |
| [Order drawer deep link](https://eto.tools/dashboard/orders/?drawer={receipt_id}) | A drawer query parameter opens that order detail drawer as soon as the page paints. | `Orders > (URL query string)` | `order_detail` |

#### List, counts and paging

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Status count pills | Counts orders that are paid and not refunded or cancelled, paid and not refunded, paid and not cancelled, just paid, unpaid, and cancelled or refunded. | `Orders > (pill row under the filters)` | - |
| Top summary bar | Shows the total order count, page count and the date span of the orders on screen. | `Orders > (bar above the list)` | - |
| Top page arrows | Steps to the previous or next page from the bar above the list. | `Orders > (bar above the list) > arrows` | - |
| Pagination | Steps through pages from the bottom of the list and shows which range of orders is on screen. | `Orders > Previous / Next` | `orders_list` |
| Reconcile bar | Reports the background check against Etsy for recent order changes, and can be stopped. | `Orders > Checking recent changes with Etsy...` (needs connected store) | - |
| No orders found | The empty state shown when the current filters match nothing. | `Orders > (empty list)` | - |
| [No Store Connected](https://eto.tools/dashboard/stores/) | The empty state with a Connect Store button, shown when you have no Etsy store attached. | `Orders > Connect Store` | - |

#### Order row

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Profit flow chips | Shows Subtotal, Fees, Gross Profit, COG, Net Profit and, when refunded, Refunded and Final Net on the row itself. | `Orders > (order row) > profit chips` | - |
| Profit chip popover | Clicking a chip opens the line-by-line breakdown behind that number. | `Orders > (order row) > Fees` | - |
| COG inline input | Types the cost of goods for the item straight into the row without opening the order. | `Orders > (order row) > COG` | - |
| SKU inline edit | Edits and saves the SKU for the item from the row, with the full Eto SKU shown when Etsy truncates it. | `Orders > (order row) > SKU` | - |
| Supplier picker | Assigns a supplier to the listing from the row, for the store or for that product. | `Orders > (order row) > Supplier` | - |
| Copy tracking | Copies the row tracking number to the clipboard. | `Orders > (order row) > (copy icon)` | - |
| Tracking status help | Opens the legend explaining No Tracking, Awaiting Tracking, Tracking Ready and Fulfilled. | `Orders > (order row) > ?` | - |
| Row indicators | Marks orders that carry a buyer Message, are a Gift, or include Custom personalisation. | `Orders > (order row) > Message` | - |
| Ship by countdown | Shows days, hours and minutes left to ship, or how long the order is overdue. | `Orders > (order row) > Ship by` | - |

#### Order detail drawer

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Overview tab | Lists receipt id, type, order date, store, buyer, payment method, paid and shipped flags, gift flag and the full money breakdown down to refunds and adjusted total. | `Orders > (order row) > Overview` | `order_detail` |
| Edit buyer field | Edits a buyer detail such as name or address line in place from the Overview tab. | `Orders > (order row) > Overview > (pencil icon)` | - |
| Items tab | Lists each item with variations, quantity, SKU, price and shipping. | `Orders > (order row) > Items` | `order_transactions` |
| Total Cost | Records one cost of goods figure for an item, including its shipping and fees. | `Orders > (order row) > Items > Total Cost` | - |
| Enter per-unit costs | Switches a multi-quantity item to one cost box per unit and totals them for you. | `Orders > (order row) > Items > Enter per-unit costs` | - |
| Use single total | Collapses per-unit costs back to a single total for the item. | `Orders > (order row) > Items > Use single total` | - |
| Tracking tab | Shows the tracking state, Printify tracking, supplier tracking entries grouped by the link they came from, and Etsy shipments. | `Orders > (order row) > Tracking` | - |
| Submit to Etsy | Sends a supplier tracking entry to Etsy as the shipment for this order, with an optional buyer message and a self-notification toggle. | `Orders > (order row) > Tracking > Submit to Etsy` (needs connected store) | `shop_order_tracking` |
| Manual Submit | Types your own tracking number and carrier and submits it to Etsy. | `Orders > (order row) > Tracking > Submit your own tracking to Etsy` (needs connected store) | `shop_order_tracking` |
| Delete tracking entry | Removes a supplier tracking entry and its history from the order. | `Orders > (order row) > Tracking > (x on an entry)` | - |
| Shipping tab | Shows the shipping address, email, phone and SKU as copyable rows, plus supplier tracking and Etsy shipments. | `Orders > (order row) > Shipping` | - |
| Copy All | Copies the whole shipping address as one block. | `Orders > (order row) > Shipping > Copy All` | - |
| Printify / Gelato Format | Copies a US address already laid out the way Printify and Gelato expect it. | `Orders > (order row) > Shipping > Copy Address` (needs US order) | - |
| Payment tab | Shows the price breakdown, payment info and every refund on the order. | `Orders > (order row) > Payment` | `order_payments` |
| Messages tab | Shows the gift information and the buyer message and personalisation text. | `Orders > (order row) > Messages` | - |
| Product tab | Shows the live listing behind the item: images, videos, title, description, details, personalisation, variations, tags and materials. | `Orders > (order row) > Product` (needs connected store) | `listing_detail` |
| Product Source Link | Saves the supplier or AliExpress URL you buy this product from. | `Orders > (order row) > Product > Product Source Link` | - |
| Notes tab | Keeps a free-text note against the order and saves it as you type. | `Orders > (order row) > Notes` | - |
| Printify tab | Connects the Etsy order to a Printify order, or shows and disconnects the one already linked. | `Orders > (order row) > Printify` (needs Printify connected) | - |

#### Supplier sharing and export

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Share with Supplier | Creates a public link that shows a supplier only the orders matching your current filters. | `Orders > Share > Create Link` (needs connected store) | - |
| Link Name | Names the supplier link so you can tell it apart later. | `Orders > Share > Link Name` | - |
| Expires | Sets the supplier link to expire after 24h, 7d, 30d, or never. | `Orders > Share > Expires` | - |
| Download CSV | Exports the currently filtered orders as a CSV file. | `Orders > Share > Download CSV` | - |
| CSV Columns | Chooses which columns the CSV includes, with select all and deselect all. | `Orders > Share > (gear icon)` | - |
| Manage Links | Lists your active supplier links and the ones in the trash. | `Orders > (link icon) > Active` | - |
| Link trash | Holds deleted supplier links so they can be restored or removed permanently. | `Orders > (link icon) > Trash` | - |

#### COGs import

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Upload File | Takes a CSV or Excel sheet of product costs as the first import step. | `Orders > Import COGs` | - |
| Currency | Says which currency the sheet is in and sets the rate to your store currency. | `Orders > Import COGs > Currency` | - |
| Select Store | Picks which store the imported costs belong to. | `Orders > Import COGs > Select Store` (needs connected store) | - |
| Preview Matches | Shows which sheet rows matched which orders before anything is written. | `Orders > Import COGs > Preview Matches` | - |

#### Supplier link page (public)

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Supplier order list](https://eto.tools/supplier/{token}/) | The page your supplier opens from a share link, showing only the orders you shared and no seller data. | `Orders > Share > Create Link > (open the link)` (needs share link) | - |
| [Supplier search](https://eto.tools/supplier/{token}/) | Searches the shared orders by order number, name, SKU, product or tracking. | `Orders > Share > Create Link > (open the link) > Search` (needs share link) | - |
| [Supplier status tabs](https://eto.tools/supplier/{token}/) | Switches between pending, overdue, done and all shared orders. | `Orders > Share > Create Link > (open the link) > pending` (needs share link) | - |
| [Supplier filters](https://eto.tools/supplier/{token}/) | Narrows the shared list by country and by product. | `Orders > Share > Create Link > (open the link) > (country select)` (needs share link) | - |
| [Supplier sort](https://eto.tools/supplier/{token}/) | Orders the shared list by ship date, order value, order date, quantity or buyer name, in either direction. | `Orders > Share > Create Link > (open the link) > Ship date` (needs share link) | - |
| [Group by product](https://eto.tools/supplier/{token}/) | Groups the shared orders under the product they are for. | `Orders > Share > Create Link > (open the link) > Group by product` (needs share link) | - |
| [Paste tracking](https://eto.tools/supplier/{token}/) | Bulk-fills tracking numbers by pasting one order number and tracking number per line. | `Orders > Share > Create Link > (open the link) > Paste tracking` (needs share link) | - |
| [Supplier export CSV](https://eto.tools/supplier/{token}/) | Downloads the shared order list as a CSV. | `Orders > Share > Create Link > (open the link) > Export CSV` (needs share link) | - |
| [Supplier bulk actions](https://eto.tools/supplier/{token}/) | With rows selected, marks them done or not done, copies their addresses, or exports just those rows. | `Orders > Share > Create Link > (open the link) > Mark done` (needs share link) | - |
| [Supplier save bar](https://eto.tools/supplier/{token}/) | Holds unsaved tracking edits and writes them back with Save or drops them with Discard. | `Orders > Share > Create Link > (open the link) > Save` (needs share link) | - |
| [Supplier keyboard shortcuts](https://eto.tools/supplier/{token}/) | Cmd or Ctrl plus S saves pending edits, the slash key jumps to search, and Escape closes the paste dialog. | `Orders > Share > Create Link > (open the link)` (needs share link) | - |
| [Supplier display controls](https://eto.tools/supplier/{token}/) | Switches to compact rows, changes the text size, flips the theme, or switches the page between English and Chinese. | `Orders > Share > Create Link > (open the link) > A+` (needs share link) | - |

### Finance

- URL: https://eto.tools/dashboard/finances/
- Anchor: https://eto.tools/dev/docs/#where-finance
- Markdown: https://eto.tools/dev/docs/where/finance.md
- Features: 69

Your Etsy money by day or date range: balance, payouts, sales, fees, ads, refunds, COGS and profit, with the ledger behind every figure.

#### First-time setup

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Fetch Financial Data | Backfills a store ledger from Etsy so Finance has something to report on. | `Finance > Fetch Financial Data` (needs connected store) | `finance_sync` |
| Backfill window | Chooses how far back the first fetch goes: 30, 60, 90 or 120 days. | `Finance > 120 days` (needs connected store) | - |
| Set up later | Skips setup for the stores still pending so the overlay stops blocking the page. | `Finance > Set up later` | - |
| Don't track | Turns a store off for Finance, the same flag the Stores page toggle sets. | `Finance > Don’t track` | - |
| Retry | Retries the backfill for a store whose first fetch failed. | `Finance > Retry` (needs connected store) | `finance_sync` |
| Setup notice | A strip naming the stores that are not tracked in Finance yet, with Set up and Dismiss. | `Finance > Set up` | - |
| Set up skipped stores | Brings back the setup prompt for stores you dismissed earlier. | `Finance > (gear icon) > Set up N skipped stores` | - |

#### Toolbar

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Store selector | Picks which store the page reports on, or All Stores to combine them. | `Finance > (store name)` (needs connected store) | - |
| All Stores | Combines every unlocked store into one set of figures, converted into your target currency. | `Finance > (store name) > All Stores` (needs two or more stores) | - |
| Date button | Opens the calendar for picking a single day or a date range to report on. | `Finance > (date label)` | - |
| Calendar picker | Tap one day for a day view or two days for a range, then Apply, or Clear to reset. | `Finance > (date label) > Apply` | - |
| Export | Exports every ledger row in the selected window as a file. | `Finance > Export` | - |
| Export CSV | Downloads the window in Etsy statement layout: date, type, title, info, currency, amount, fees and taxes, net and tax details. | `Finance > Export > CSV` | `finance` |
| Export Excel workbook | Downloads an .xlsx with real number columns and one sheet per store. | `Finance > Export > Excel workbook` | `finance` |
| Copy JSON | Copies the figures currently on screen to the clipboard as JSON. | `Finance > (copy icon)` | `finance` |

#### Lens and verification

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Settled | Counts money on the day it actually moved. | `Finance > Settled` | - |
| Outcome | Counts what each day of sales eventually turned out to be worth after refunds and cancellations. | `Finance > Outcome` | - |
| Verification strip | Reports whether the window on screen has been checked against Etsy, and repairs missing entries when it finds them. | `Finance > (strip above the figures)` (needs connected store) | - |
| Try again | Re-runs verification for stores that could not be checked. | `Finance > (verification strip) > Try again` | - |

#### Settings panel

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Refresh Today | Re-fetches today from Etsy for the selected store and repaints the day. | `Finance > (gear icon) > Today` (needs connected store) | `finance_sync` |
| Refresh Previous 7 Days | Re-fetches the last seven days from Etsy. | `Finance > (gear icon) > Previous 7 Days` (needs connected store) | `finance_sync` |
| Refresh Previous 30 Days | Re-fetches the last thirty days from Etsy. | `Finance > (gear icon) > Previous 30 Days` (needs connected store) | `finance_sync` |
| Refresh Range | Opens a day-by-day picker so you can re-fetch only the days you choose. | `Finance > (gear icon) > Range...` (needs connected store) | `finance_sync` |
| Adjust ad spend timing | Moves ad charges onto the day they were incurred rather than the day Etsy billed them. | `Finance > (gear icon) > Adjust ad spend timing` (needs single store selected) | - |
| Remove all unpaid sales from data | Drops cancelled orders that were never paid out of every figure on the page. | `Finance > (gear icon) > Remove all unpaid sales from data` | - |
| Convert currency | Shows every figure in another currency at a rate you set. | `Finance > (gear icon) > Convert currency` | - |
| Configure rate | Sets the target currency code and the multiplier used for conversion. | `Finance > (gear icon) > Configure rate` | - |
| Delete all finance data | Wipes every ledger entry, fetch window, profit snapshot and cached day, after you type DELETE. | `Finance > (gear icon) > Delete all finance data` | - |

#### Currency rates and subscriptions

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Currency settings | Opens the settings dialog used to combine stores that bill in different currencies. | `Finance > (gear icon) > Currency settings` (needs two or more stores) | - |
| Target currency | Sets the single currency that All Stores figures are reported in. | `Finance > (gear icon) > Currency settings > Target currency` | - |
| Store rates | Sets the exchange rate used for each store currency. | `Finance > (gear icon) > Currency settings > Store rates` | - |
| Auto-fill rates | Fills in the per-store exchange rates for you. | `Finance > (gear icon) > Currency settings > Auto-fill rates` | - |
| Manage subscriptions | Records the tools and services you pay for outside Etsy so they count against All Stores profit. | `Finance > (gear icon) > Manage subscriptions` | `finance_software_expenses` |
| Add subscription | Adds a recurring or one-time software cost with a name, amount, currency, frequency and start date. | `Finance > (gear icon) > Manage subscriptions > Add` | `finance_software_expenses` |

#### Day view

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Current Balance | Your Etsy payment account balance at the end of the selected day. | `Finance > Current Balance` (needs connected store) | `finance` |
| Paid Out | What Etsy deposited to your bank on the selected day. | `Finance > Paid Out` | `finance` |
| Sales | Gross sales taken on the day, and the number of sales behind it. | `Finance > Sales` | `finance` |
| Orders not paid | Orders placed on the day that never produced any ledger activity. | `Finance > Orders not paid` | - |
| Refunds | Money refunded to buyers on the day. | `Finance > Refunds` | `finance` |
| Cancelled | Value of orders cancelled on the day. | `Finance > Cancelled` | `finance` |
| Recoupment | Money Etsy is recovering from your balance to cover a cancellation or refund. | `Finance > Recoupment` | `finance` |
| Credits | Refund credits Etsy returned to you. | `Finance > Credits` | `finance` |
| Subscription | Etsy subscription charges falling in the window. | `Finance > Subscription` | `finance` |
| Net Profit | Gross profit after cost of goods. | `Finance > Net Profit` | `finance` |
| COGS | Total cost of goods for the sales in the window, taken from the costs you entered on Orders. | `Finance > COGS` | `finance` |
| Gross Profit | Sales minus Etsy fees and ads, before cost of goods. | `Finance > Gross Profit` | `finance` |
| Fees Paid | Every Etsy fee charged in the window. | `Finance > Fees Paid` | `finance` |
| Ads Spent | Etsy ads and offsite ads charged in the window. | `Finance > Ads Spent` | `finance` |
| Metric breakdown | Opens the line-by-line entries behind any card that carries the breakdown button. | `Finance > Net Profit > (breakdown icon)` | - |
| Balance chart | Plots the running balance through the day against the selected metrics. | `Finance > (chart under the cards)` | - |
| Chart metric toggles | Turns Balance, Net Profit, Gross sales, COGS, Fees paid, Ads spent, Refunds, Cancelled, Payout and Sales lines on or off. | `Finance > (chart) > Net Profit` | - |
| Transaction groups | Lists every ledger entry for the day, grouped into the sale, fee and refund set it belongs to. | `Finance > (list under the chart)` | `finance` |
| Saved days | Opens days already stored in your database that are older than the last seven, with a search box. | `Finance > Saved days` | - |

#### Range summary

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| All 7 Days | Switches from one day to a seven day summary of the same figures. | `Finance > All 7 Days` | `finance` |
| All 30 Days | Switches to a thirty day summary once those days are loaded. | `Finance > All 30 Days` | `finance` |
| Range | Picks a custom start and end day for the summary, saved days included. | `Finance > Range` | `finance` |
| Trends chart | Plots the selected metrics day by day across the chosen range. | `Finance > (trends chart)` | - |
| Profit per product | Ranks listings over the range with sales, gross, fees, refunds, COGS, net and margin. | `Finance > Profit per product` | `finance` |
| Products view | Shows the range as one row per listing, and clicking a row opens that listing orders. | `Finance > Profit per product > Products` | - |
| All Transactions | Shows every transaction group in the range instead of the per-product rollup. | `Finance > Profit per product > All Transactions` | - |
| Issues | Lists the transaction groups that could not be categorised automatically so you can check them. | `Finance > Profit per product > Issues` | - |
| Table search | Filters the transaction or group table by description, type, date or amount. | `Finance > Profit per product > All Transactions > Search transactions...` | - |
| Table paging | Steps through the transaction and group tables a page at a time. | `Finance > Profit per product > Next` | - |

#### All Stores view

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| All Stores tab | Shows the combined figures for every selected store as one set of cards. | `Finance > (store name) > All Stores > All Stores` (needs two or more stores) | `finance` |
| Per Store tab | Splits the same window back out into one row of figures per store. | `Finance > (store name) > All Stores > Per Store` (needs two or more stores) | `finance` |
| Software Expense | The subscriptions you logged, charged against the selected window. | `Finance > (store name) > All Stores > Software Expense` (needs two or more stores) | `finance_software_expenses` |
| Software expenses breakdown | Opens the individual subscription charges that make up the Software Expense figure. | `Finance > Software Expense > (breakdown icon)` | `finance_software_expenses` |
| Per-store breakdown | Lists each store gross profit and its own cards under the combined totals. | `Finance > (store name) > All Stores > Per-store breakdown` (needs two or more stores) | `finance` |
| Multi-store refresh | Re-fetches every selected store from Etsy at once, with live per-store progress. | `Finance > (store name) > All Stores > (gear icon) > Previous 7 Days` (needs two or more stores) | `finance_sync` |

### Stores

- URL: https://eto.tools/dashboard/stores/
- Anchor: https://eto.tools/dev/docs/#where-stores
- Markdown: https://eto.tools/dev/docs/where/stores.md
- Features: 45

Connect Etsy shops to Eto and control what each one shows on the rest of the app.

#### Connecting a store

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Add store | Starts the Etsy OAuth flow and opens the connect panel with the authorisation link. | `Stores > Add store` | - |
| Connect your Etsy store panel | Shows the generated Etsy authorisation link and waits for Etsy to call back. | `Stores > Add store > Connect your Etsy store` | - |
| Copy link | Copies the Etsy authorisation URL to the clipboard and starts polling for the callback. | `Stores > Add store > Copy link` | - |
| Cancel | Closes the connect panel and stops listening for the Etsy callback. | `Stores > Add store > Cancel` | - |
| Connected shop | Shows the shop name Etsy returned once the callback lands, then reloads the store list. | `Stores > Add store > Connected shop` | - |
| Store connection limit warning | Blocks a new connection and offers an upgrade when the plan store limit is already used up. | `Stores > Add store` | - |

#### Store list

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Your stores | Lists every connected shop as a card with its icon, shop id and active listing count. | `Stores` | `stores_list` |
| Refresh from Etsy | Re-pulls the shop list from Etsy and updates the cards in place. | `Stores > Refresh from Etsy` (needs connected store) | `store_sync` |
| Search stores | Filters the cards by shop name as you type; only appears once two or more stores are connected. | `Stores > Search stores...` | - |
| Clear search | Empties the search field and shows every store again. | `Stores > Search stores... > Clear search` | - |
| All | Shows every connected store and carries a live count. | `Stores > All` | - |
| Hidden | Shows only stores that are hidden from at least one page or dashboard panel. | `Stores > Hidden` | - |
| Select all | Ticks every store currently on screen, so a filter or search narrows what gets selected. | `Stores > Select all` | - |

#### Store card

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Store name link](https://eto.tools/dashboard/stores/{shop_id}/) | Opens the per-store detail page; only clickable once details have been loaded. | `Stores > store card > shop name` | `store_detail` |
| Load details | Fetches the shop profile from Etsy for a store that has never been synced. | `Stores > store card > Load details` (needs connected store) | `store_sync` |
| Refresh store details | Re-fetches that one shop profile from Etsy and updates its card. | `Stores > store card > refresh icon` (needs connected store) | `store_sync` |
| Disconnect store | Opens the disconnect confirmation for that store. | `Stores > store card > close icon` | - |
| Disconnect store? modal | Confirms removing the store from Eto; the card disappears at once and the cascade runs in the background. | `Stores > store card > close icon > Disconnect` | - |
| Order notifications | Switch that turns on a notification in Eto whenever that store takes an order. | `Stores > store card > Order notifications` (needs connected store) | - |
| Visibility | Expands the per-store panel of switches that hide the shop from individual pages. | `Stores > store card > Visibility` | - |
| Hide on pages: Orders | Hides this store from the Orders page. | `Stores > store card > Visibility > Hide on pages > Orders` | - |
| Hide on pages: Finance | Hides this store from the Finance page. | `Stores > store card > Visibility > Hide on pages > Finance` | - |
| Hide on pages: Listings | Hides this store from the Listing Manager store chips. | `Stores > store card > Visibility > Hide on pages > Listings` | - |
| Hide on dashboard: Orders | Hides this store from the dashboard order feeds. | `Stores > store card > Visibility > Hide on dashboard > Orders` | - |
| Hide on dashboard: Finances | Hides this store from the dashboard finance figures. | `Stores > store card > Visibility > Hide on dashboard > Finances` | - |
| Hide on dashboard: Entire store | Hides the store from the dashboard completely and locks the two dashboard sub-switches on. | `Stores > store card > Visibility > Hide on dashboard > Entire store` | - |

#### Bulk actions

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Selected count bar | Floating bar that appears as soon as one store card is ticked. | `Stores > store card checkbox` | - |
| Select all | Selects every store that passes the current filter and search. | `Stores > store card checkbox > Select all` | - |
| Clear | Drops the whole selection. | `Stores > store card checkbox > Clear` | - |
| Notifications on | Turns order notifications on for every selected store at once. | `Stores > store card checkbox > Notifications on` (needs connected store) | - |
| Notifications off | Turns order notifications off for every selected store at once. | `Stores > store card checkbox > Notifications off` (needs connected store) | - |
| Visibility menu | Opens a menu that hides or shows every selected store on one page at a time. | `Stores > store card checkbox > Visibility` | - |
| Hide on pages rows | Show and Hide buttons that set Orders, Finance or Listings visibility for the whole selection. | `Stores > store card checkbox > Visibility > Hide on pages` | - |
| Hide on dashboard rows | Show and Hide buttons that set dashboard Orders, Finances or Entire store visibility for the whole selection. | `Stores > store card checkbox > Visibility > Hide on dashboard` | - |

#### Store detail page

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Load Store Stats](https://eto.tools/dashboard/stores/{shop_id}/) | Fetches the shop profile, stats and shipping profiles from Etsy for a store with no data yet. | `Stores > store card > shop name > Load Store Stats` (needs connected store) | `store_sync` |
| [Refresh](https://eto.tools/dashboard/stores/{shop_id}/) | Re-pulls this shop profile from Etsy and reloads the page with the fresh figures. | `Stores > store card > shop name > Refresh` (needs connected store) | `store_sync` |
| [View on Etsy](https://eto.tools/dashboard/stores/{shop_id}/) | Opens the public shop page on Etsy in a new tab. | `Stores > store card > shop name > View on Etsy` | - |
| [Stats row](https://eto.tools/dashboard/stores/{shop_id}/) | Shows active listings, total sales, favourers and review average for the shop. | `Stores > store card > shop name` | `store_details_cached` |
| [Order Notifications](https://eto.tools/dashboard/stores/{shop_id}/) | Switch that turns Etsy order webhooks for this store into Eto notifications. | `Stores > store card > shop name > Order Notifications` (needs connected store) | - |
| [Shop Information](https://eto.tools/dashboard/stores/{shop_id}/) | Lists currency, shipping-from country, location, languages and the shop creation date. | `Stores > store card > shop name > Shop Information` | `store_details_cached` |
| [Shop Status](https://eto.tools/dashboard/stores/{shop_id}/) | Shows vacation mode, custom requests, Etsy Payments and Direct Checkout state. | `Stores > store card > shop name > Shop Status` | `store_details_cached` |
| [Shipping Profiles](https://eto.tools/dashboard/stores/{shop_id}/) | Lists each saved shipping profile with processing and delivery times, method, carrier and cost. | `Stores > store card > shop name > Shipping Profiles` (needs connected store) | `shipping_profiles_live` |
| [Shop Announcement](https://eto.tools/dashboard/stores/{shop_id}/) | Shows the shop announcement text pulled from Etsy. | `Stores > store card > shop name > Shop Announcement` | `store_details_cached` |
| [Digital Sale Message](https://eto.tools/dashboard/stores/{shop_id}/) | Shows the message Etsy sends to buyers of digital items. | `Stores > store card > shop name > Digital Sale Message` | `store_details_cached` |
| Back to stores | Returns to the store list. | `Stores > store card > shop name > back arrow` | - |

### Listing Manager

- URL: https://eto.tools/dashboard/listings/
- Anchor: https://eto.tools/dev/docs/#where-listing-manager
- Markdown: https://eto.tools/dev/docs/where/listing-manager.md
- Features: 74

Browse, filter and bulk edit every listing across your connected Etsy shops.

#### Modes

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Default | Normal browsing mode where clicking a listing opens it. | `Listing Manager > Default` | - |
| Edit Products | Turns the grid into a selection surface and shows the bulk action bar. | `Listing Manager > Edit Products` | - |
| Edit Storewide | Replaces the grid with a store picker for settings that apply to a whole shop. | `Listing Manager > Edit Storewide` | - |

#### Toolbar

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Refresh All | Re-pulls listings from Etsy for every unlocked store one after another with a per-store progress panel. | `Listing Manager > Refresh All` (needs connected store) | `store_sync` |
| Rank stores | Re-orders the store chip row by Listings, Sales, Orders, Newest store, Oldest store or Name A-Z. | `Listing Manager > Rank stores` | - |
| Grid view | Shows listings as a grid of image cards. | `Listing Manager > Grid view` | - |
| List view | Shows listings as compact rows with inline action buttons. | `Listing Manager > List view` | - |
| Columns | Sets how many cards per row the grid uses, from 3 to 8, and remembers the choice. | `Listing Manager > Columns` | - |

#### Store chips

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Store chip | Selects a shop and loads its listings, showing the shop icon and its listing count. | `Listing Manager > store chip` (needs connected store) | `stores_list` |
| Locked store chip | Shows a padlock and an Upgrade Now link for shops beyond the plan store limit. | `Listing Manager > store chip` | - |
| Printify chip | Appears alongside the Etsy shops when a Printify API token and shop id are saved. | `Listing Manager > Printify chip` (needs Printify API token) | - |
| Load listings from Etsy | Manual retry that pulls this store from Etsy when nothing has loaded yet. | `Listing Manager > store chip > Load listings from Etsy` (needs connected store) | `store_sync` |
| Checking for updates | Runs by itself after picking a store and quietly adds new listings and state changes to the grid. | `Listing Manager > store chip` (needs connected store) | - |

#### Filters, search and sort

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| All / Active / Inactive / Draft | Status tabs that filter the grid and show a live count for each state. | `Listing Manager > All` | - |
| All Sections | Filters the grid to one Etsy shop section. | `Listing Manager > All Sections` (needs connected store) | `shop_sections_live` |
| All Types | Filters to digital or physical listings. | `Listing Manager > All Types` | - |
| Sort by | Sorts the loaded listings by Quantity, Views, Favorites, Sold, Recently Bought, Price, Created Date or Modified Date. | `Listing Manager > Sort by` | - |
| Sort direction | Flips the sort between highest first and lowest first. | `Listing Manager > Sort by > sort direction arrow` | - |
| All Ages | Filters by listing age: last 7 days, 30 days, 90 days, last year or older than a year. | `Listing Manager > All Ages` | - |
| Search | Filters listings by title, with an instant dropdown of matches you can walk with the arrow keys and open with Enter. | `Listing Manager > Search...` | `listings_search` |
| Refresh | Re-pulls just the selected store from Etsy and streams the progress. | `Listing Manager > Refresh` (needs connected store) | `store_sync` |
| Previous / Next | Pages through the listings when a store has more than one page. | `Listing Manager > Next` | - |

#### Listing card and row actions

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| View on Etsy | Opens the live listing on Etsy in a new tab. | `Listing Manager > listing card > Etsy icon` | - |
| [Edit listing](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Opens the full edit workspace for that listing; ctrl or cmd click opens it in a new tab. | `Listing Manager > listing card > Edit` (needs connected store) | `listing_detail` |
| Copy listing | Copies the listing into a new workspace draft, using media Eto has stored so it still works for a shop that is gone. | `Listing Manager > listing card > Copy` | - |
| Upload to Etsy | Publishes a single draft listing live on Etsy for $0.20. | `Listing Manager > listing card > Upload to Etsy` (needs connected store) | - |
| Delete from Etsy | Deletes that one listing from Etsy after a confirmation with a tick box. | `Listing Manager > listing card > Delete` (needs connected store) | `listing_delete` |
| Sold count | Fills the sold figure on each card in the background after the grid renders. | `Listing Manager > listing card` (needs connected store) | - |

#### Edit Products bulk actions

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Select All | Ticks every listing currently loaded for the store. | `Listing Manager > Edit Products > Select All` | - |
| Draft / Active / Inactive quick select | Selects every loaded listing in that state, and clicking the same button again clears the selection. | `Listing Manager > Edit Products > Draft` | - |
| Edit Processing Profiles | Changes the Etsy processing profile on every selected listing, with a progress bar and error list. | `Listing Manager > Edit Products > Edit Processing Profiles` (needs connected store) | `processing_profiles_live` |
| Edit Quantity | Opens a per-listing stock editor for the selection, with a Set all to field and a Apply button. | `Listing Manager > Edit Products > Edit Quantity` (needs connected store) | `listing_inventory_update` |
| Show variations | Expands a listing inside Edit Quantity to set stock per variation instead of one number. | `Listing Manager > Edit Products > Edit Quantity > chevron` (needs connected store) | `listing_inventory_update` |
| Copy to Store | Copies the selected listings into another connected shop, applying that shop shipping profile, return policy and processing profile. | `Listing Manager > Edit Products > Copy to Store` (needs connected store) | `listing_create` |
| Copy to store as draft | Destination option that creates the copies as free drafts on the target shop. | `Listing Manager > Edit Products > Copy to Store > Copy to store as draft` (needs connected store) | `listing_create` |
| Publish to store | Destination option that creates and activates the copies live on Etsy at $0.20 each. | `Listing Manager > Edit Products > Copy to Store > Publish to store` (needs connected store) | `listing_create` |
| Activate on Etsy | Publishes every selected draft live on Etsy, and only turns on when the whole selection is drafts. | `Listing Manager > Edit Products > Activate on Etsy` (needs connected store) | - |
| Don't show this warning again | Hides the $0.20 activation warning on future bulk activations. | `Listing Manager > Edit Products > Activate on Etsy > Don't show this warning again` | - |
| Delete from Etsy | Deletes every selected listing from Etsy after ticking the cannot be undone box. | `Listing Manager > Edit Products > Delete from Etsy` (needs connected store) | `listing_delete` |

#### Edit Storewide and quantity automation

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Select All Stores | Ticks every unlocked Etsy shop in the storewide list. | `Listing Manager > Edit Storewide > Select All Stores` | - |
| Store row | One row per shop with a tick box, listing count and the automation badge when a rule covers it. | `Listing Manager > Edit Storewide > store row` (needs connected store) | - |
| Auto Quantity Control | Opens the automation that puts a listing quantity back up on its own after it sells down. | `Listing Manager > Edit Storewide > Auto Quantity Control` (needs connected store) | - |
| Auto Listing Quantity Control | The rule window: it watches the selected shops and resets stock whenever an order drops a listing to the threshold. | `Listing Manager > Edit Storewide > Auto Quantity Control > Auto Listing Quantity Control` (needs connected store) | - |
| Condition | Sets the quantity at or below which the rule fires, written as If listing quantity is less than or equal to a number. | `Listing Manager > Edit Storewide > Auto Quantity Control > Condition` (needs connected store) | - |
| Action | Sets the quantity the listing is put back to, written as Set quantity to a number. | `Listing Manager > Edit Storewide > Auto Quantity Control > Action` (needs connected store) | - |
| Affected Stores | Lists the shops the rule will run on, taken from the storewide selection. | `Listing Manager > Edit Storewide > Auto Quantity Control > Affected Stores` (needs connected store) | - |
| Automation Active | Green panel showing how many times the rule has fired and when it last fired. | `Listing Manager > Edit Storewide > Auto Quantity Control > Automation Active` (needs connected store) | - |
| Save Automation | Saves the rule; the target must be higher than the threshold and at least one store must be picked. | `Listing Manager > Edit Storewide > Auto Quantity Control > Save Automation` (needs connected store) | - |
| Delete Rule | Removes the quantity automation entirely. | `Listing Manager > Edit Storewide > Auto Quantity Control > Delete Rule` (needs connected store) | - |
| Auto Qty badge | Chip on a store row showing the live rule as threshold to target, with edit and remove buttons. | `Listing Manager > Edit Storewide > store row` (needs connected store) | - |
| Edit rule | Reopens the automation window for the shops the rule already covers. | `Listing Manager > Edit Storewide > store row > edit icon` (needs connected store) | - |
| Remove automation | Takes one shop out of the quantity rule without deleting the rule. | `Listing Manager > Edit Storewide > store row > close icon` (needs connected store) | - |

#### Listing detail page

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Listing detail](https://eto.tools/dashboard/listings/{listing_id}/) | Read-only page for one listing with its images, price, status and saved fields. | `Listing Manager > listing card` | `listing_detail` |
| [Thumbnail gallery](https://eto.tools/dashboard/listings/{listing_id}/) | Swaps the main image when you click one of the listing thumbnails. | `Listing Manager > listing card > thumbnail` | `listing_images` |
| [Etsy](https://eto.tools/dashboard/listings/{listing_id}/) | Opens this listing on Etsy in a new tab, or shows a plain chip when there is no live URL. | `Listing Manager > listing card > Etsy` | - |
| [Listing Info](https://eto.tools/dashboard/listings/{listing_id}/) | Lists type, who made it, when made, supply, processing days, variations, auto-renew, language and listing id. | `Listing Manager > listing card > Listing Info` | `listing_detail` |
| [Personalization](https://eto.tools/dashboard/listings/{listing_id}/) | Shows whether the listing is personalisable or customisable, the character limit and the buyer instructions. | `Listing Manager > listing card > Personalization` | `listing_personalization` |
| [Dimensions](https://eto.tools/dashboard/listings/{listing_id}/) | Shows the item weight and length, width and height with their units. | `Listing Manager > listing card > Dimensions` | `listing_detail` |
| [Description](https://eto.tools/dashboard/listings/{listing_id}/) | Shows the saved listing description. | `Listing Manager > listing card > Description` | `listing_detail` |
| [Tags](https://eto.tools/dashboard/listings/{listing_id}/) | Shows every tag on the listing with a count. | `Listing Manager > listing card > Tags` | `listing_detail` |
| [Materials](https://eto.tools/dashboard/listings/{listing_id}/) | Shows the materials saved on the listing. | `Listing Manager > listing card > Materials` | `listing_detail` |
| [Inventory](https://eto.tools/dashboard/listings/{listing_id}/) | Lists each variation with its SKU, price and stock, or Unavailable when it is turned off. | `Listing Manager > listing card > Inventory` | `listing_inventory_get` |
| [Timestamps](https://eto.tools/dashboard/listings/{listing_id}/) | Shows when the listing was created, last modified and when it ends. | `Listing Manager > listing card > Timestamps` | `listing_detail` |

#### Edit listing workspace

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Editing listing](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | The full workspace opened in edit mode against a live Etsy listing, saving a local draft copy as you work. | `Listing Manager > listing card > Edit` (needs connected store) | `listing_detail` |
| [About](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Tab for the title, photos, video, description and personalisation questions. | `Listing Manager > listing card > Edit > About` (needs connected store) | `listing_image_upload` |
| [Add product image](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Adds or replaces the listing photos in the About tab. | `Listing Manager > listing card > Edit > About > Add product image` (needs connected store) | `listing_image_upload` |
| [Personalisation question types](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Choose a Text box, List of options, File upload or labelled File upload for buyer input. | `Listing Manager > listing card > Edit > About > personalisation` (needs connected store) | `listing_personalization` |
| [Price & Inventory](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Tab for price, quantity, SKU, shop discount and the variation grid. | `Listing Manager > listing card > Edit > Price & Inventory` (needs connected store) | `listing_inventory_update` |
| [Details](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Tab for category, category attributes, who made it, when made, tags and materials. | `Listing Manager > listing card > Edit > Details` (needs connected store) | `category_properties` |
| [Settings](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Section for the target stores, processing profile, shop section and production partners. | `Listing Manager > listing card > Edit > Settings` (needs connected store) | `shop_sections_live` |
| [Update Listing](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Pushes the edits back to Etsy; the button reads Update Listing whenever the listing still exists there. | `Listing Manager > listing card > Edit > Update Listing` (needs connected store) | `shop_listing_update` |
| [Activate Listing ($0.20)](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Extra button on a draft listing that publishes it live on Etsy for $0.20. | `Listing Manager > listing card > Edit > Activate Listing ($0.20)` (needs connected store) | - |
| [Publish as Draft](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Replaces Update Listing when the original was deleted from Etsy, rebuilding it as a new draft from Eto stored copy. | `Listing Manager > listing card > Edit > Publish as Draft` (needs connected store) | `listing_create` |
| [Sanity strip](https://eto.tools/dashboard/listings/{shop_id}/{listing_id}/edit/) | Running checklist across the top for at least one photo, a shipping profile and at least one store. | `Listing Manager > listing card > Edit` (needs connected store) | - |

### Quick upload

- URL: https://eto.tools/dashboard/single-research/
- Anchor: https://eto.tools/dev/docs/#where-quick-upload
- Markdown: https://eto.tools/dev/docs/where/quick-upload.md
- Features: 104

Build Etsy listings in Eto and push them to one or more connected stores as drafts.

#### Listing list

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Active | Shows listings already pushed to Etsy, with a count badge, and they are view only. | `Quick upload > Active` (needs connected store) | - |
| Drafts | Shows listings saved in Eto but not sent to Etsy yet, with a count badge. | `Quick upload > Drafts` | - |
| Scheduled | Shows single listings and bulk groups queued to publish at a set time. | `Quick upload > Scheduled` | - |
| [New listing](https://eto.tools/dashboard/single-research/new/) | Opens the create screen where you pick how the listing starts. | `Quick upload > New listing` | - |
| Select all | Ticks every listing card in the open tab so bulk actions apply to them. | `Quick upload > Select all` | - |
| Publish selected (n) | Publishes the ticked drafts to Etsy one at a time and turns into a Stop button while it runs. | `Quick upload > Select all > Publish selected` (needs connected store) | `listing_create` |
| Delete selected (n) | Deletes every ticked listing from Eto in one call. | `Quick upload > Select all > Delete selected` | - |
| Columns | Sets how many listing cards sit in a row, from 2 up to 10. | `Quick upload > Columns` | - |
| Open on Etsy | Opens the published listing on Etsy in a new tab. | `Quick upload > Active > Open on Etsy` (needs published listing) | - |
| Copy | Duplicates the listing into a new draft and opens that draft. | `Quick upload > Copy` | - |
| Delete | Removes the listing from Eto without touching Etsy. | `Quick upload > Delete` | - |
| View group | Opens a modal listing every listing in a bulk group, marked Main or Variant, each with Open on Etsy and Delete. | `Quick upload > Active > View group` (needs bulk group) | - |
| Delete group | Deletes every listing that belongs to a bulk group in one go. | `Quick upload > Active > Delete group` (needs bulk group) | - |
| Cancel | Cancels a scheduled single listing and returns it to drafts. | `Quick upload > Scheduled > Cancel` (needs scheduled listing) | - |
| Cancel schedule | Cancels a scheduled bulk group before it publishes. | `Quick upload > Scheduled > Cancel schedule` (needs scheduled bulk group) | - |
| Bulk uploads in progress | Lists background bulk upload jobs with live progress, and failed jobs get a dismiss cross. | `Quick upload` (needs running bulk job) | - |
| Edit & retry | Appears on a card that failed during bulk publish and opens that listing in the workspace. | `Quick upload > Publish selected` | - |

#### Creating a listing

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Enter Listing ID](https://eto.tools/dashboard/single-research/new/) | Pulls an existing Etsy listing by its numeric ID and pre-fills a new draft from it. | `Quick upload > New listing > Enter Listing ID > Continue` | - |
| [Digital Product](https://eto.tools/dashboard/single-research/new/) | Creates a blank digital draft that asks for downloadable files instead of shipping. | `Quick upload > New listing > Start from Scratch > Digital Product` | - |
| [Physical Product](https://eto.tools/dashboard/single-research/new/) | Creates a blank physical draft that asks for shipping and processing profiles. | `Quick upload > New listing > Start from Scratch > Physical Product` | - |
| [Upload spreadsheet](https://eto.tools/dashboard/single-research/csv-import/) | Starts the CSV or Excel import wizard for creating many listings at once. | `Quick upload > New listing > Upload via CSV / Excel > Upload spreadsheet` | - |
| [Format guide](https://eto.tools/dev/docs/#bulk-csv-import) | Opens the bulk CSV import section of the developer docs in a new tab. | `Quick upload > New listing > Upload via CSV / Excel > Format guide` | - |
| [Your Favorites](https://eto.tools/dashboard/single-research/new/) | Lists products you saved from Product Hunt so you can start a listing from one. | `Quick upload > New listing > Your Favorites` (needs saved favorite) | - |

#### Workspace: About

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [About](https://eto.tools/dashboard/single-research/{listing_pk}/) | Tab holding the title, photos, description and personalization fields. | `Quick upload > open a listing > About` | - |
| [Title](https://eto.tools/dashboard/single-research/{listing_pk}/) | Free text listing title with a live 0/140 character counter. | `Quick upload > open a listing > About > Title` | - |
| [AI Magic](https://eto.tools/dashboard/single-research/{listing_pk}/) | Writes the title with AI, and clicking it again opens a Special requirements box you send with Enter. | `Quick upload > open a listing > About > Title > AI Magic` (needs AI provider configured) | `ai_generate_title` |
| [Undo AI title](https://eto.tools/dashboard/single-research/{listing_pk}/) | Puts the previous title back after an AI rewrite. | `Quick upload > open a listing > About > Title > Undo AI title` | - |
| [Photos & video](https://eto.tools/dashboard/single-research/{listing_pk}/) | Twelve slot media grid where slot 1 is the primary photo and slot 2 is an optional video under 15 seconds. | `Quick upload > open a listing > About > Photos & video` | - |
| [Add photo](https://eto.tools/dashboard/single-research/{listing_pk}/) | Opens the file picker for that slot, accepting JPG, PNG or GIF up to 100MB and up to 20 photos. | `Quick upload > open a listing > About > Photos & video > Add photo` | `file_upload` |
| [Add video](https://eto.tools/dashboard/single-research/{listing_pk}/) | Uploads one MP4 or MOV into the video slot. | `Quick upload > open a listing > About > Photos & video > Add video` | `file_upload` |
| [Add from Gallery](https://eto.tools/dashboard/single-research/{listing_pk}/) | Picks a saved Gallery image straight into an empty photo slot. | `Quick upload > open a listing > About > Photos & video > Add from Gallery` (needs saved gallery image) | - |
| [AI Image](https://eto.tools/dashboard/single-research/{listing_pk}/) | Hover button on a photo that opens the per image AI generator. | `Quick upload > open a listing > About > hover a photo > AI Image` (needs AI provider configured) | `ai_generate_image` |
| [Preset](https://eto.tools/dashboard/single-research/{listing_pk}/) | Chooses the AI image recipe: Custom, Thumbnail, Info or Showcase. | `Quick upload > open a listing > About > hover a photo > AI Image > Preset` | - |
| [Model](https://eto.tools/dashboard/single-research/{listing_pk}/) | Picks the image model between Gemini 3 Pro, Gemini 3.1 Flash and Gemini 2.5 Flash. | `Quick upload > open a listing > About > hover a photo > AI Image > Model` | - |
| [Use Product Image](https://eto.tools/dashboard/single-research/{listing_pk}/) | Switch that decides whether the AI works from your uploaded product photo or from the tile you clicked. | `Quick upload > open a listing > About > hover a photo > AI Image > Use Product Image` | - |
| [Gallery](https://eto.tools/dashboard/single-research/{listing_pk}/) | Hover button on a photo that swaps it for a saved Gallery image. | `Quick upload > open a listing > About > hover a photo > Gallery` (needs saved gallery image) | - |
| [Download image](https://eto.tools/dashboard/single-research/{listing_pk}/) | Item in the photo three dot menu that saves that image to your computer. | `Quick upload > open a listing > About > hover a photo > More > Download image` | - |
| [Set as product image](https://eto.tools/dashboard/single-research/{listing_pk}/) | Item in the photo three dot menu that promotes the image to the reference product image. | `Quick upload > open a listing > About > hover a photo > More > Set as product image` | - |
| [Undo AI edit](https://eto.tools/dashboard/single-research/{listing_pk}/) | Item in the photo three dot menu that steps back through AI edits of that photo. | `Quick upload > open a listing > About > hover a photo > More > Undo AI edit` (needs AI edit applied) | - |
| [Generate Slideshow Video](https://eto.tools/dashboard/single-research/{listing_pk}/) | Turns the listing photos into an MP4 slideshow and drops it into the video slot. | `Quick upload > open a listing > About > Generate Slideshow Video` (needs at least one photo) | - |
| [Slideshow settings](https://eto.tools/dashboard/single-research/{listing_pk}/) | Sets total video length, per image duration and which photos take part, with a Distribute equally shortcut. | `Quick upload > open a listing > About > Slideshow settings` | - |
| [Product Image](https://eto.tools/dashboard/single-research/{listing_pk}/) | Single square reference image the AI tools use as the source product. | `Quick upload > open a listing > About > Product Image` | - |
| [+ Add Custom Image](https://eto.tools/dashboard/single-research/{listing_pk}/) | Uploads an extra named image, such as a sizing chart, that AI prompts can refer to by name. | `Quick upload > open a listing > About > + Add Custom Image` | - |
| [Description](https://eto.tools/dashboard/single-research/{listing_pk}/) | Long form listing description with its own AI Magic button and undo. | `Quick upload > open a listing > About > Description` | `ai_generate_description` |
| [Add new field](https://eto.tools/dashboard/single-research/{listing_pk}/) | Adds a personalization field of type Text box, List of options, File upload or File upload (labeled), up to five. | `Quick upload > open a listing > About > Personalization > Add new field` | - |
| [Digital files](https://eto.tools/dashboard/single-research/{listing_pk}/) | Drag and drop area for up to five downloadable files, 100MB each, on digital listings only. | `Quick upload > open a digital listing > About > Digital files` (needs digital listing) | `file_upload` |

#### Workspace: Price & Inventory

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Price & Inventory](https://eto.tools/dashboard/single-research/{listing_pk}/) | Tab holding price, quantity, SKU and the variations editor. | `Quick upload > open a listing > Price & Inventory` | - |
| [Price](https://eto.tools/dashboard/single-research/{listing_pk}/) | One price box per selected store, published in that store currency. | `Quick upload > open a listing > Price & Inventory > Price` (needs connected store) | - |
| [Shop discount](https://eto.tools/dashboard/single-research/{listing_pk}/) | Percentage that back calculates the listed price so buyers see the price you typed. | `Quick upload > open a listing > Price & Inventory > Shop discount` | - |
| [Quantity](https://eto.tools/dashboard/single-research/{listing_pk}/) | Stock number for the listing, between 1 and 999. | `Quick upload > open a listing > Price & Inventory > Quantity` | - |
| [SKU](https://eto.tools/dashboard/single-research/{listing_pk}/) | Internal identifier where the first 32 characters go to Etsy and the full string stays in Eto. | `Quick upload > open a listing > Price & Inventory > SKU` | - |
| [+ Add Variation](https://eto.tools/dashboard/single-research/{listing_pk}/) | Creates a variation such as Colour or Size, up to the Etsy maximum of two. | `Quick upload > open a listing > Price & Inventory > + Add Variation` | - |
| [Prices vary for each](https://eto.tools/dashboard/single-research/{listing_pk}/) | Switch that turns on a separate price per option combination. | `Quick upload > open a listing > Price & Inventory > Variations > Prices vary for each` (needs variations) | - |
| [Quantities vary for each](https://eto.tools/dashboard/single-research/{listing_pk}/) | Switch that turns on a separate stock count per option combination. | `Quick upload > open a listing > Price & Inventory > Variations > Quantities vary for each` (needs variations) | - |
| [SKUs vary for each](https://eto.tools/dashboard/single-research/{listing_pk}/) | Switch that turns on a separate SKU per option combination. | `Quick upload > open a listing > Price & Inventory > Variations > SKUs vary for each` (needs variations) | - |
| [Processing profiles vary for each](https://eto.tools/dashboard/single-research/{listing_pk}/) | Switch that lets each option combination use its own processing profile. | `Quick upload > open a listing > Price & Inventory > Variations > Processing profiles vary for each` (needs variations) | - |
| [Photos vary for each](https://eto.tools/dashboard/single-research/{listing_pk}/) | Switch that links a listing photo to each option value. | `Quick upload > open a listing > Price & Inventory > Variations > Photos vary for each` (needs variations) | - |
| [Generate SKUs](https://eto.tools/dashboard/single-research/{listing_pk}/) | Describes your SKU format in plain text and lets AI fill every variation SKU, with an option to preserve hand written ones. | `Quick upload > open a listing > Price & Inventory > Variations > SKU Generator > Generate SKUs` (needs AI provider configured) | - |
| [Swap variation order](https://eto.tools/dashboard/single-research/{listing_pk}/) | Flips which variation is first and which is second. | `Quick upload > open a listing > Price & Inventory > Variations > Swap variation order` (needs two variations) | - |
| [Back to single price](https://eto.tools/dashboard/single-research/{listing_pk}/) | Drops the variations and returns the listing to one price and quantity. | `Quick upload > open a listing > Price & Inventory > Variations > Back to single price` (needs variations) | - |

#### Workspace: Details

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Details](https://eto.tools/dashboard/single-research/{listing_pk}/) | Tab holding category, category attributes, who and when made, tags and materials. | `Quick upload > open a listing > Details` | - |
| [Category](https://eto.tools/dashboard/single-research/{listing_pk}/) | Opens a searchable Etsy category tree and sets the listing taxonomy. | `Quick upload > open a listing > Details > Category` | `categories` |
| [Category attributes](https://eto.tools/dashboard/single-research/{listing_pk}/) | Shows the extra fields Etsy requires for the chosen category. | `Quick upload > open a listing > Details > Category attributes` (needs category selected) | `category_properties` |
| [When was it made?](https://eto.tools/dashboard/single-research/{listing_pk}/) | Sets the Etsy production era, from Made To Order through to vintage decades. | `Quick upload > open a listing > Details > When was it made?` | - |
| [Tags](https://eto.tools/dashboard/single-research/{listing_pk}/) | Adds up to 13 search tags of 20 characters each, comma separated. | `Quick upload > open a listing > Details > Tags` | - |
| [Materials](https://eto.tools/dashboard/single-research/{listing_pk}/) | Lists the materials the product is made from. | `Quick upload > open a listing > Details > Materials` | - |

#### Workspace: Settings sidebar

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Stores](https://eto.tools/dashboard/single-research/{listing_pk}/) | Ticks which connected stores the listing publishes to, and locked rows link to Upgrade Now. | `Quick upload > open a listing > Settings > Stores` (needs connected store) | `stores_list` |
| [Shipping profile](https://eto.tools/dashboard/single-research/{listing_pk}/) | Picks a shipping profile per store, required for physical listings. | `Quick upload > open a listing > Settings > Shipping profile` (needs physical listing) | `shipping_profiles_live` |
| [Processing profile](https://eto.tools/dashboard/single-research/{listing_pk}/) | Picks a readiness profile per store, and Create processing profile makes a new one from a type and processing time. | `Quick upload > open a listing > Settings > Processing profile` (needs physical listing) | `processing_profiles_live` |
| [Return policy](https://eto.tools/dashboard/single-research/{listing_pk}/) | Picks a return policy per store, with a Refresh button after you add one on Etsy. | `Quick upload > open a listing > Settings > Return policy` (needs connected store) | `return_policies_live` |
| [Shop section](https://eto.tools/dashboard/single-research/{listing_pk}/) | Picks the shop section per store, and the Create box adds a new section to that shop. | `Quick upload > open a listing > Settings > Shop section` (needs connected store) | `shop_sections_live` |
| [Production partners](https://eto.tools/dashboard/single-research/{listing_pk}/) | Selects the Etsy production partners for each store. | `Quick upload > open a listing > Settings > Production partners` (needs connected store) | - |

#### Workspace: publish, schedule and status

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Publish draft](https://eto.tools/dashboard/single-research/{listing_pk}/) | Creates the listing as a draft in each selected store and uploads its media and files. | `Quick upload > open a listing > Publish draft` (needs connected store) | `listing_create` |
| [Missing](https://eto.tools/dashboard/single-research/{listing_pk}/) | Bar at the top of the tabs naming the first requirement still blocking publish. | `Quick upload > open a listing` | - |
| [Schedule 1 listing](https://eto.tools/dashboard/single-research/{listing_pk}/) | Opens a date, time and timezone picker and queues this one listing to publish later. | `Quick upload > open a listing > Schedule > Schedule 1 listing` (needs connected store) | - |
| [Schedule bulk group](https://eto.tools/dashboard/single-research/{listing_pk}/) | Queues the whole generated bulk group to publish at a chosen date and time. | `Quick upload > open a listing > Schedule > Schedule bulk group` (needs bulk group built) | - |
| [AI provider badge](https://eto.tools/dashboard/single-research/{listing_pk}/) | Shows which AI credentials are in use and tests the connection when clicked. | `Quick upload > open a listing > AI provider badge` | - |
| [AI Generations](https://eto.tools/dashboard/single-research/{listing_pk}/) | Collapsible panel listing every AI image job running on this listing with its status. | `Quick upload > open a listing > AI Generations` (needs AI job running) | - |

#### Bulk upload from one listing

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Bulk Upload](https://eto.tools/dashboard/single-research/{listing_pk}/) | Button beside Publish draft that turns one finished listing into several near copies, enabled only once the listing is ready to publish. | `Quick upload > open a listing > Bulk Upload` (needs listing ready to publish) | - |
| [Count](https://eto.tools/dashboard/single-research/{listing_pk}/) | Sets how many extra variants to generate, between 2 and 20. | `Quick upload > open a listing > Bulk Upload > Count` | - |
| [Image model](https://eto.tools/dashboard/single-research/{listing_pk}/) | Chooses the model that draws the variant thumbnails, including a Custom model ID option. | `Quick upload > open a listing > Bulk Upload > Image model` | - |
| [Custom image prompt](https://eto.tools/dashboard/single-research/{listing_pk}/) | Replaces the default thumbnail prompt for the whole batch. | `Quick upload > open a listing > Bulk Upload > Custom image prompt` | - |
| [Custom title prompt](https://eto.tools/dashboard/single-research/{listing_pk}/) | Replaces the default title prompt for the whole batch. | `Quick upload > open a listing > Bulk Upload > Custom title prompt` | - |
| [Continue](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Opens the Bulk Upload Preview page and starts generating the variant images and titles in parallel. | `Quick upload > open a listing > Bulk Upload > Continue` | `ai_generate_image` |
| [Bulk Upload Preview](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Shows the Main (your custom) card next to the generated variant rows, each marked Generating, Ready or Error. | `Quick upload > open a listing > Bulk Upload > Continue` | - |
| [Regenerate title](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Redraws one variant title inside the variant edit modal. | `Quick upload > open a listing > Bulk Upload > Continue > click a variant > Regenerate title` | `ai_generate_title` |
| [Regenerate image](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Redraws one variant thumbnail, optionally from a short prompt you type. | `Quick upload > open a listing > Bulk Upload > Continue > click a variant > Regenerate image` | `ai_generate_image` |
| [Variation Prices](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Edits the price of each variation for a variant listing, applied to every selected shop. | `Quick upload > open a listing > Bulk Upload > Continue > click a variant > Variation Prices` (needs variations) | - |
| [Upload all](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Creates every variant plus the main listing as drafts in the selected stores in one background job. | `Quick upload > open a listing > Bulk Upload > Continue > Upload all` (needs connected store) | `listing_create` |
| [Schedule bulk group](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Queues the whole generated group to publish at a chosen date, time and timezone. | `Quick upload > open a listing > Bulk Upload > Continue > Schedule bulk group` (needs connected store) | - |
| [Retry failed](https://eto.tools/dashboard/single-research/{listing_pk}/bulk-upload/) | Regenerates only the variants whose image or title failed. | `Quick upload > open a listing > Bulk Upload > Continue > Retry failed` (needs a failed variant) | - |
| Back to listings | Leaves the builder and returns to the Quick upload list while the job keeps running. | `Quick upload > open a listing > Bulk Upload > Continue > Back to listings` | - |

#### CSV and Excel import

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Upload your spreadsheet](https://eto.tools/dashboard/single-research/csv-import/) | Step one of the wizard, where you drop a .csv, .xlsx or .xlsm file, including an Etsy bulk edit export. | `Quick upload > New listing > Upload spreadsheet` | - |
| [Download template](https://eto.tools/api/single-research/csv-import/template/) | Downloads eto_listing_import_template.csv with the canonical headers and two example rows. | `Quick upload > New listing > Upload spreadsheet > Download template` | - |
| [Just a couple of details](https://eto.tools/dashboard/single-research/csv-import/) | Step that only appears when the file left out a store, shipping profile, processing profile or return policy, and applies your choice to every missing row. | `Quick upload > New listing > Upload spreadsheet > Continue` | - |
| [Review & edit](https://eto.tools/dashboard/single-research/csv-import/) | Step three, showing every parsed listing with counts of ready, warnings and blocked rows sorted worst first. | `Quick upload > New listing > Upload spreadsheet > Continue > Continue` | - |
| [Table](https://eto.tools/dashboard/single-research/csv-import/) | Review view with an editable cell per field: title, type, category, price, quantity, SKU, tags, materials, who made, when made, shipping, processing, return, section and photos. | `Quick upload > New listing > Upload spreadsheet > Review & edit > Table` | - |
| [Cards](https://eto.tools/dashboard/single-research/csv-import/) | Review view showing each listing as a thumbnail card with its status badge and reasons. | `Quick upload > New listing > Upload spreadsheet > Review & edit > Cards` | - |
| [Choose category](https://eto.tools/dashboard/single-research/csv-import/) | Fix button on a blocked row that opens a searchable Etsy category picker. | `Quick upload > New listing > Upload spreadsheet > Review & edit > Choose category` (needs row with a category error) | `categories` |
| [Photo cell](https://eto.tools/dashboard/single-research/csv-import/) | Steps through each photo URL on a row and lets you edit or add one inline. | `Quick upload > New listing > Upload spreadsheet > Review & edit > Table` | - |
| [Save changes](https://eto.tools/dashboard/single-research/csv-import/) | Revalidates your edits on the server before the wizard will let you upload. | `Quick upload > New listing > Upload spreadsheet > Review & edit > Save changes` (needs unsaved edits) | - |
| [Select all ready listings](https://eto.tools/dashboard/single-research/csv-import/) | Ticks every row that passed validation so only those get uploaded. | `Quick upload > New listing > Upload spreadsheet > Review & edit > Select all ready listings` | - |
| [Upload n to draft](https://eto.tools/dashboard/single-research/csv-import/) | Runs the import, creating each selected row as a draft listing with a live progress bar and per row result. | `Quick upload > New listing > Upload spreadsheet > Review & edit > Upload n to draft` (needs connected store) | `listing_create` |
| Open Quick upload | Ends the wizard and returns to the listing list once the import has finished. | `Quick upload > New listing > Upload spreadsheet > Open Quick upload` | - |

### VA Admin

- URL: https://eto.tools/dashboard/va-admin/
- Anchor: https://eto.tools/dev/docs/#where-va-admin
- Markdown: https://eto.tools/dev/docs/where/va-admin.md
- Features: 40

Create virtual assistant logins, set what each one may do per store, and review what they have done.

#### Assistant list

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Search virtual assistants | Filters the assistant cards as you type. | `VA Admin > Search virtual assistants` | - |
| [Add VA](https://eto.tools/dashboard/va-admin/add/) | Opens the form for creating a new assistant login. | `VA Admin > Add VA` | - |
| Assistant card | Shows the assistant name, email, active status, join date and a summary of their Orders, Products and Product Hunt access. | `VA Admin` | - |
| [Edit](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Opens that assistant for editing. | `VA Admin > Edit` | - |
| Delete | Asks to confirm, then removes the assistant and revokes their access to every store. | `VA Admin > Delete` | - |

#### Add assistant

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Private Name](https://eto.tools/dashboard/va-admin/add/) | Names the assistant for your eyes only, so you can tell several apart. | `VA Admin > Add VA > Identity` | - |
| [Display Name](https://eto.tools/dashboard/va-admin/add/) | Sets the name the assistant sees when they sign in. | `VA Admin > Add VA > Identity` | - |
| [First Name and Last Name](https://eto.tools/dashboard/va-admin/add/) | Records the person behind the account. | `VA Admin > Add VA > Identity` | - |
| [Email Address](https://eto.tools/dashboard/va-admin/add/) | Sets the address the assistant signs in with. | `VA Admin > Add VA > Login Credentials` | - |
| [Password](https://eto.tools/dashboard/va-admin/add/) | Sets the assistant password, with a show/hide toggle and a live strength meter. | `VA Admin > Add VA > Login Credentials` | - |
| [Gemini API Key](https://eto.tools/dashboard/va-admin/add/) | Optionally gives the assistant their own Gemini key instead of using the owner AI provider. | `VA Admin > Add VA > VA API Keys` | - |
| [Product Hunt](https://eto.tools/dashboard/va-admin/add/) | Toggles whether the assistant may use the Etsy product research tool. | `VA Admin > Add VA > Product Hunt` | - |
| [Store Permissions](https://eto.tools/dashboard/va-admin/add/) | Picks which stores the assistant can work in, with a running selected count. | `VA Admin > Add VA > Store Permissions` (needs connected store) | - |

#### Edit assistant

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Details tab](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Edits identity, password and the Gemini key; the email address is fixed after creation. | `VA Admin > Edit > Details` | - |
| [Active toggle](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Suspends or restores the assistant login without deleting the account. | `VA Admin > Edit > Details > Account` | - |
| [Order Permissions card](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Opens the per-store order permission editor and shows how many stores are covered. | `VA Admin > Edit > Details > Order Permissions` | - |
| [Upload Permissions card](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Opens the per-store upload permission editor and shows how many stores are covered. | `VA Admin > Edit > Details > Upload Permissions` | - |
| [Product Hunt toggle](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Turns the research tool on or off for this assistant. | `VA Admin > Edit > Details > Product Hunt` | - |
| [Delete VA](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Asks to confirm, then deletes the assistant and their store access. | `VA Admin > Edit > Delete` | - |

#### Activity log

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Login Status](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Shows whether the assistant is signed in now and when they last were. | `VA Admin > Edit > Activity > Login Status` | - |
| [Etsy Searches](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Lists the product searches the assistant ran, with a count. | `VA Admin > Edit > Activity > Etsy Searches` | - |
| [Listings Uploaded](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Lists the listings the assistant published, with a count. | `VA Admin > Edit > Activity > Listings Uploaded` | - |
| [Orders Fulfilled](https://eto.tools/dashboard/va-admin/{va_id}/edit/) | Lists the orders the assistant completed, with a count. | `VA Admin > Edit > Activity > Orders Fulfilled` | - |

#### Order permissions page

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Search stores](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Filters the store rows on the permission page. | `VA Admin > Edit > Order Permissions > Search stores` | - |
| [Enable All](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Turns every order permission on for every listed store. | `VA Admin > Edit > Order Permissions > Enable All` | - |
| [Disable All](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Turns every order permission off for every listed store. | `VA Admin > Edit > Order Permissions > Disable All` | - |
| [View Orders](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Lets the assistant see the order list and order details for that store. | `VA Admin > Edit > Order Permissions` | - |
| [Create Links](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Lets the assistant create supplier share links. | `VA Admin > Edit > Order Permissions > Supplier` | - |
| [View Tracking](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Lets the assistant see supplier tracking entries. | `VA Admin > Edit > Order Permissions > Supplier` | - |
| [Delete Links](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Lets the assistant remove supplier share links. | `VA Admin > Edit > Order Permissions > Supplier` | - |
| [Delete Variations](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Lets the assistant remove supplier tracking entries. | `VA Admin > Edit > Order Permissions > Supplier` | - |
| [Complete Orders](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Lets the assistant mark an order complete through a supplier. | `VA Admin > Edit > Order Permissions > Supplier` | - |
| [Unsaved changes](https://eto.tools/dashboard/va-admin/{va_id}/permissions/orders/) | Warns that permission edits are still pending, cleared by Save. | `VA Admin > Edit > Order Permissions` | - |

#### Upload permissions page

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Upload Products](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Lets the assistant upload single products to that store. | `VA Admin > Edit > Upload Permissions` | - |
| [Bulk Upload](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Lets the assistant upload many listings at once. | `VA Admin > Edit > Upload Permissions` | - |
| [Product Type](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Restricts the assistant to all products, digital only or physical only. | `VA Admin > Edit > Upload Permissions` | - |
| [View Active Listings](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Lets the assistant see the owner active listings for that store. | `VA Admin > Edit > Upload Permissions` | - |
| [View Drafts](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Lets the assistant see the owner draft listings. | `VA Admin > Edit > Upload Permissions` | - |
| [View Scheduled](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Lets the assistant see the owner scheduled listings. | `VA Admin > Edit > Upload Permissions` | - |
| [Save](https://eto.tools/dashboard/va-admin/{va_id}/permissions/uploads/) | Writes the whole permission set for every store at once. | `VA Admin > Edit > Upload Permissions > Save` | - |

### Product Hunt

- URL: https://eto.tools/dashboard/etsy-search/
- Anchor: https://eto.tools/dev/docs/#where-product-hunt
- Markdown: https://eto.tools/dev/docs/where/product-hunt.md
- Features: 39

Searches the Etsy marketplace for a keyword, scores every listing for 24h demand, and shows the results as a filterable card grid.

#### Search

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Search Product Hunt listings | Text box for the keyword you want to research on Etsy. | `Product Hunt > Search Product Hunt listings` (needs connected store) | `product_hunt` |
| Search button | Starts the hunt for the keyword in the box; pressing Enter in the box does the same. | `Product Hunt > Search` (needs connected store) | `product_hunt` |
| Search settings | Icon button that opens the dropdown holding the four search options. | `Product Hunt > Search settings` (needs connected store) | - |
| Amount of Products | Sets how many listings the hunt scans, from 1 up to 10000. | `Product Hunt > Search settings > Amount of Products` (needs connected store) | `product_hunt` |
| Max Listing Age (months) | Keeps only listings created within the last N months; blank means no age limit. | `Product Hunt > Search settings > Max Listing Age (months)` (needs connected store) | `product_hunt` |
| Full Product Details | Switch that runs the slower deep scan so every card carries full listing data. | `Product Hunt > Search settings > Full Product Details` (needs connected store) | `product_hunt` |
| Optimized Mode | Switch, on by default, that lets the AI drop off-topic listings and rank the rest; it disables Full Product Details while on. | `Product Hunt > Search settings > Optimized Mode` (needs connected store) | `product_hunt` |
| Settings summary strip | One line under the search box repeating the amount, age limit and any mode that is on. | `Product Hunt > Search settings` (needs connected store) | - |
| Progress bar | Bar that narrates each phase of the run, from collecting listings to pruning, demand and ranking. | `Product Hunt > Search` (needs connected store) | - |
| No Store Connected empty state | Replaces the whole page with a Connect Store prompt when the account has no Etsy store. | `Product Hunt` | - |
| Daily limit reached popup | Blocks the search and shows how many hunts of the daily allowance are used when the plan limit is hit. | `Product Hunt > Search` | - |

#### Filters and sorting

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Sort by | Orders the results by demand, listing age, shop age, price, favorites, views, rating, reviews, quantity, shop sales, estimated sales per month or estimated revenue per month. | `Product Hunt > Sort by` (needs connected store) | - |
| Ranked Score | Extra sort option added after an Optimized Mode run that orders cards by the AI opportunity score. | `Product Hunt > Sort by > Ranked Score` (needs connected store) | - |
| Sort direction | Flips the chosen sort between high to low and low to high. | `Product Hunt > Sort by` (needs connected store) | - |
| Type | Shows only digital or only physical listings. | `Product Hunt > Type` (needs connected store) | - |
| Variations | Shows only listings with variations or only listings without them. | `Product Hunt > Variations` (needs connected store) | - |
| Ships from | Country menu filled from the countries actually present in the results. | `Product Hunt > Ships from` (needs connected store) | - |
| Search within results | Text box that narrows the visible cards to titles containing what you type. | `Product Hunt > Search` (needs connected store) | - |
| Column count | Buttons 2 to 7 that set how many cards sit in a row, remembered in the browser. | `Product Hunt > column buttons` (needs connected store) | - |
| Prev and Next | Page through the result grid using the pager above and below the cards. | `Product Hunt > Next` (needs connected store) | - |

#### Result cards

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Demand badge | Flame badge on each card showing units sold in the last 24 hours, tinted zero, low, medium or high. | `Product Hunt > card` (needs connected store) | - |
| Ranked score badge | Second badge shown after an Optimized Mode run carrying the AI score out of 100, with its reason on hover. | `Product Hunt > card` (needs connected store) | - |
| AI reason | Short sentence at the foot of the card explaining why the AI ranked that listing where it did. | `Product Hunt > card` (needs connected store) | - |
| ID pill | Click the listing ID on a card to copy it to the clipboard. | `Product Hunt > card > ID` (needs connected store) | - |
| Etsy link | Corner link that opens the listing on Etsy in a new tab without opening the drawer. | `Product Hunt > card > Etsy` (needs connected store) | - |
| Favourite heart | Saves the listing to the Quick Thoughts favourites list in the sidebar. | `Product Hunt > card > heart` (needs connected store) | - |
| Card meta pills | Row of pills under each card holding created date, shop age, shop sales, ships-from country, views and favourites. | `Product Hunt > card` (needs connected store) | - |
| Estimated sales and revenue pills | Two pills estimating monthly sales and revenue from demand, reviews, views and price, or two question marks when there is no demand reading. | `Product Hunt > card` (needs connected store) | - |
| Pill tooltips | Hovering a pill shows the exact shop opening date, the full ships-from country name, or a warning that the estimates are approximate. | `Product Hunt > card` (needs connected store) | - |

#### Listing detail drawer

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Open detail drawer | Clicking a card slides in a drawer with the full listing, closed with Escape or the close button. | `Product Hunt > card` (needs connected store) | - |
| Drawer stats | Top block listing created date, shop age, shop sales, ships from, views, favorers and the two estimates. | `Product Hunt > card > drawer` (needs connected store) | - |
| Drawer facts | Definition list holding listing ID, type, quantity, price, last modified date and shop name. | `Product Hunt > card > drawer` (needs connected store) | - |
| Copy title, description, tags and materials | Each text section in the drawer has its own copy button. | `Product Hunt > card > drawer > Copy` (needs connected store) | - |
| Offerings | Collapsible table of every variation offering with its price, quantity and enabled state, plus a Retry when inventory has not loaded. | `Product Hunt > card > drawer > Offerings` (needs connected store) | - |
| Images | Collapsible image slider with prev and next arrows, a counter and thumbnails. | `Product Hunt > card > drawer > Images` (needs connected store) | - |
| Reviews | Collapsible table of up to fifty reviews with rating, language and text. | `Product Hunt > card > drawer > Reviews` (needs connected store) | - |
| Open on Etsy | Foot link that opens the listing page on Etsy. | `Product Hunt > card > drawer > Open on Etsy` (needs connected store) | - |

#### Privacy and history

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Always incognito | Product Hunt results are held in the browser only and never written to the database, so leaving or reloading the page loses them. | `Product Hunt` | - |
| Search summary saved to history | After a run the keyword and its headline numbers are pushed to the sidebar search history. | `Product Hunt > Search` (needs full tracking enabled on the account) | - |

### Shop Spy

- URL: https://eto.tools/dashboard/shop-spy/
- Anchor: https://eto.tools/dev/docs/#where-shop-spy
- Markdown: https://eto.tools/dev/docs/where/shop-spy.md
- Features: 20

Takes any Etsy listing link and pulls that whole shop, then lets you sort and filter every active listing it has.

#### Lookup

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Paste an Etsy product URL or listing ID | Input that accepts a full Etsy listing link or the bare numeric listing ID. | `Shop Spy > URL box` | - |
| Analyse shop | Looks up the shop behind that listing and starts loading every active listing it has. | `Shop Spy > Analyse shop` | - |
| Progress bar | Reports the two loading phases, collecting listing IDs and then hydrating their details. | `Shop Spy > Analyse shop` | - |

#### Shop bar

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Shop summary line | One row with the shop icon, name, listing count, sales, favourites, rating and review count. | `Shop Spy > Analyse shop` | - |
| Visit Shop | Opens the shop page on Etsy in a new tab. | `Shop Spy > Visit Shop` | - |

#### Filters and sorting

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Sort by | Orders the shop listings by created date, price, views, favorites or quantity. | `Shop Spy > Sort by` | - |
| Sort direction | Flips the chosen sort between high to low and low to high. | `Shop Spy > Sort by` | - |
| Type | Shows only the digital or only the physical listings in the shop. | `Shop Spy > Type` | - |
| Variations | Shows only listings with variations or only listings without them. | `Shop Spy > Variations` | - |
| Search | Narrows the grid to listings whose title or description contains what you type. | `Shop Spy > Search` | - |
| Column count | Buttons 3 to 7 that set how many cards sit in a row. | `Shop Spy > column buttons` | - |
| Previous and Next | Pages through the listings 48 at a time using the pager above and below the grid. | `Shop Spy > Next` | - |

#### Listing cards and drawer

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Listing card | Card per listing with image, title, price and pills for created date, views, favourites, quantity, digital or physical and whether it has variations. | `Shop Spy > card` | - |
| ID pill | Click the listing ID on a card to copy it to the clipboard. | `Shop Spy > card > ID` | - |
| Etsy link | Corner link that opens the listing on Etsy without opening the drawer. | `Shop Spy > card > Etsy` | - |
| Detail drawer | Clicking a card opens a drawer with listing ID, created and modified dates, price, type, quantity, favorers, views, who made, when made, language, full title and description. | `Shop Spy > card` | - |
| Offerings | Collapsible table in the drawer showing each offering with price, quantity and enabled state. | `Shop Spy > card > Offerings` | - |
| Images | Collapsible image slider in the drawer with arrows, a counter and thumbnails. | `Shop Spy > card > Images` | - |
| Reviews | Collapsible table in the drawer of up to fifty reviews with rating, language and text. | `Shop Spy > card > Reviews` | - |
| Open on Etsy | Link inside the drawer to the listing page on Etsy. | `Shop Spy > card > Etsy Link` | - |

### Gallery

- URL: https://eto.tools/dashboard/gallery/
- Anchor: https://eto.tools/dev/docs/#where-gallery
- Markdown: https://eto.tools/dev/docs/where/gallery.md
- Features: 34

Chat with Gemini to generate, edit and keep product imagery for your listings.

#### Chat and modes

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Auto | Reads what you typed and decides between a text answer and an image, triggering on words like generate, create, make, draw, design, render, image, photo, mockup or thumbnail. | `Gallery > Auto` | - |
| Text | Chats only and never generates an image. | `Gallery > Text` | `ai_chat` |
| Image | Treats every message as an image request, and is the default. | `Gallery > Image` | `ai_generate_image` |
| Compare | Runs every model at once and shows the results side by side in one row. | `Gallery > Compare` | - |
| Start with a prompt | Welcome screen with three ready made prompts: Product photo on linen, White-background cutout and Listing thumbnail. | `Gallery` | - |
| New Chat | Clears the transcript and starts a fresh conversation. | `Gallery > New Chat` | - |
| Load earlier messages | Prepends the previous page of 30 stored messages to the transcript. | `Gallery > Load earlier messages` (needs a longer chat) | - |
| Thoughts | Collapsible block under a reply showing the model reasoning when the model returns it. | `Gallery > Thoughts` | - |
| Sources | Row of source links shown under a reply that used Google Search grounding. | `Gallery > Web search > Sources` (needs Web search on) | - |

#### Composer

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Files | Attaches up to four images to the message, and you can also paste or drag and drop them. | `Gallery > Files` | - |
| Settings | Opens the in card drawer holding Model, Aspect Ratio and Resolution. | `Gallery > Settings` | - |
| Model | Picks between 3 Pro Image, 3.1 Flash Image, 2.5 Flash Image, 2.5 Flash (text) and 2.5 Pro (text). | `Gallery > Settings > Model` | - |
| Aspect Ratio | Sets the output shape to 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3 or 21:9. | `Gallery > Settings > Aspect Ratio` | - |
| Resolution | Sets the output size to 1K, 2K or 4K. | `Gallery > Settings > Resolution` | - |
| Web search | Lets the model check Google Search before answering a text question. | `Gallery > Web search` | - |
| Annotate Image | Opens the annotator so you can mark up an attached or generated image before asking for a change. | `Gallery > Annotate Image` (needs an attached or generated image) | - |
| Send | Sends the message, and Enter does the same while Shift+Enter adds a new line. | `Gallery > Send` | - |

#### Annotator

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Draw circle | Circles an area of the image so you can describe what belongs there. | `Gallery > Annotate Image > Draw circle` | - |
| Draw rectangle | Boxes an area of the image. | `Gallery > Annotate Image > Draw rectangle` | - |
| Freehand draw | Draws a free shape on the image. | `Gallery > Annotate Image > Freehand draw` | - |
| Clear annotations | Wipes every mark you have drawn. | `Gallery > Annotate Image > Clear annotations` | - |
| Apply Annotations | Flattens the marked up image and adds it to the message attachments. | `Gallery > Annotate Image > Apply Annotations` | - |
| Select Image to Annotate | Picker shown when more than one image could be annotated, listing each Upload and the Last Generated image. | `Gallery > Annotate Image` (needs more than one candidate image) | - |

#### Image actions

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Download | Saves the generated image to your computer as ai-studio-<timestamp>.png. | `Gallery > hover a generated image > Download` | - |
| Save | Stores the image in your saved gallery so other Eto pages can use it. | `Gallery > hover a generated image > Save` | - |
| Full size | Opens the image in the lightbox, which also has Download, Save to gallery and Use as reference. | `Gallery > hover a generated image > Full size` | - |
| Reference | Adds the image back into the composer as context for the next message, which is how you edit an image. | `Gallery > hover a generated image > Reference` | - |

#### Saved images

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Gallery | Header button that opens the Saved Images library with a count badge. | `Gallery > Gallery` | - |
| Saved Images | Paged grid of 12 saved images per page, each showing its save date. | `Gallery > Gallery` (needs a saved image) | - |
| Delete image | Removes one saved image from the library after a confirmation. | `Gallery > Gallery > Delete image` (needs a saved image) | - |
| Previous / Next | Moves between pages of saved images. | `Gallery > Gallery > Next` (needs more than one page) | - |

#### Usage

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| % of context | Meter under the composer showing how much of the model context window this chat is using. | `Gallery` | - |
| Usage | Panel opened from the context meter showing Tokens in, Tokens out, Cost this chat, Cost today, Messages and Images. | `Gallery > % of context` | - |
| Per message | Collapsible table inside the usage panel listing model, tokens in and out, images, time and cost for every turn. | `Gallery > % of context > Per message` (needs at least one message) | - |

### Workflow

- URL: https://eto.tools/dashboard/workflow/
- Anchor: https://eto.tools/dev/docs/#where-workflow
- Markdown: https://eto.tools/dev/docs/where/workflow.md
- Features: 29

Puts Product Hunt, AI Studio and Quick Upload side by side in three resizable panes so a product can go from research to published without changing page.

#### The three panes

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Pane 1 Product Hunt | Left pane embedding the Product Hunt page so you can search Etsy and pick a product to copy. | `Workflow > Unified Workflow` | - |
| Pane 2 AI Studio | Middle pane embedding the Gallery so you can generate the images and copy for that product. | `Workflow > Unified Workflow` | - |
| Pane 3 Quick Upload | Right pane embedding Quick Upload so you can publish the finished listing to a store. | `Workflow > Unified Workflow` (needs connected store) | - |
| Step status chip | Each pane header carries a chip counting what that step has produced so far, or saying no store is connected on pane 3. | `Workflow > Unified Workflow` | - |
| Connect a store | Link in the pane 3 header shown only while the account has no Etsy store. | `Workflow > Unified Workflow > Connect a store` | - |
| Reload this step | Refresh button in each pane header that reloads only that pane. | `Workflow > Unified Workflow > reload` | - |
| Hide this step | Close button that removes a pane and gives its width to the panes that remain. | `Workflow > Unified Workflow > close` | - |
| Restore hidden panes | Counter button in the top bar that lists the hidden panes so you can bring one back. | `Workflow > Unified Workflow > restore` | - |
| Drag dividers to resize | Dragging the bars between panes changes their widths, and the percentage on each header updates as you drag. | `Workflow > Unified Workflow` | - |

#### Pane handoffs

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Product to Quick Product Search | Clicking a Product Hunt card inside Workflow opens the Quick Product Search panel on that listing instead of the usual drawer. | `Workflow > Unified Workflow > pane 1 card` | - |
| Open a listing in AI Studio | Pane 1 can hand an Etsy listing straight to the AI Studio pane to start a build from it. | `Workflow > Unified Workflow` | - |
| Open the workspace in pane 3 | When AI Studio finishes a listing it loads that listing workspace into the Quick Upload pane. | `Workflow > Unified Workflow` | - |
| Frame session badge | A pane header shows the keyword of the hunt running inside it and its status. | `Workflow > Unified Workflow` | - |

#### Quick tools

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Keywords | Top bar button that opens the Quick Keyword Search panel over the workspace. | `Workflow > Unified Workflow > Keywords` | - |
| Product | Top bar button that opens the Quick Product Search panel for pulling one Etsy listing by link or ID. | `Workflow > Unified Workflow > Product` | - |
| Thoughts | Top bar button that opens the Quick Thoughts panel for notes, favourites and history. | `Workflow > Unified Workflow > Thoughts` | - |
| Theme toggle | Switches the page between light and dark and pushes the change into every embedded pane. | `Workflow > Unified Workflow > theme` | - |
| Dashboard | Leaves the workspace and returns to the dashboard. | `Workflow > Unified Workflow > Dashboard` | - |

#### Custom layout mode

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Custom mode | Top bar toggle that swaps the fixed three panes for a workspace you fill with your own frames. | `Workflow > Unified Workflow > custom mode` | - |
| Add a Product Hunt frame | Adds another Product Hunt frame to the custom workspace. | `Workflow > Unified Workflow > custom mode > add hunt` | - |
| Add an AI Studio frame | Adds another AI Studio frame to the custom workspace. | `Workflow > Unified Workflow > custom mode > add studio` | - |
| Add a Quick Upload frame | Adds another Quick Upload frame to the custom workspace. | `Workflow > Unified Workflow > custom mode > add upload` | - |
| Columns | Number box from 1 to 12 that sets how many frames sit in a row. | `Workflow > Unified Workflow > custom mode > Columns` | - |
| Frame counter | Shows how many frames are open against the cap of 100. | `Workflow > Unified Workflow > custom mode` | - |
| Drag to reorder a frame | The grip in a frame header lets you drag it into a different position without reloading it. | `Workflow > Unified Workflow > custom mode > frame` | - |
| Drag to set frame width | The handle on a frame edge sets its width, with a live percentage while you drag. | `Workflow > Unified Workflow > custom mode > frame` | - |
| Reload this frame | Refresh button in a custom frame header that reloads only that frame. | `Workflow > Unified Workflow > custom mode > frame > reload` | - |
| Close this frame | Removes one custom frame and rebalances the rest. | `Workflow > Unified Workflow > custom mode > frame > close` | - |
| Layout is remembered | The mode, the frames, their widths, the column count and any hidden panes are all kept in the browser between visits. | `Workflow > Unified Workflow` | - |

### Developer API console

- URL: https://eto.tools/dashboard/api/
- Anchor: https://eto.tools/dev/docs/#where-developer-api-console
- Markdown: https://eto.tools/dev/docs/where/developer-api-console.md
- Features: 29

Where an Enterprise account creates its API key, connects an MCP client, and configures and watches outbound webhooks.

#### Overview

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Requests today | Shows todays request count against the daily quota with a fill bar. | `Developer API console > Overview` (needs Enterprise plan) | - |
| This month | Shows the request count for the current month. | `Developer API console > Overview` (needs Enterprise plan) | - |
| Latency | Shows the average response time for your API calls. | `Developer API console > Overview` (needs Enterprise plan) | - |
| Errors | Shows the error rate across your recent API calls. | `Developer API console > Overview` (needs Enterprise plan) | - |
| Usage chart | Charts daily requests over the last 7 or 30 days. | `Developer API console > Overview` (needs Enterprise plan) | - |
| Top endpoints | Ranks the endpoints you call most. | `Developer API console > Overview` (needs Enterprise plan) | - |
| Generate API key | Creates the account API key and shows the raw value once in a modal. | `Developer API console > Overview > Generate API key` (needs Enterprise plan) | - |
| Rotate key | Issues a new key and stops the old one working. | `Developer API console > Overview > Rotate key` (needs API key) | - |
| Revoke key | Deletes the key so anything using it stops working at once. | `Developer API console > Overview > Revoke key` (needs API key) | - |
| Key details | Shows the key prefix, creation date, last use and requests used against the daily limit. | `Developer API console > Overview` (needs API key) | - |
| Quickstart | Shows a ready curl call against the stores endpoint with your key filled in. | `Developer API console > Overview` (needs API key) | `stores_list` |
| Recent requests | Tables your latest API calls with endpoint, status and timing. | `Developer API console > Overview` (needs API key) | - |

#### MCP Connect

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Claude CLI command | Shows the claude mcp add command pointing at mcp.eto.tools with your key header. | `Developer API console > MCP Connect` (needs Enterprise plan) | - |
| Generate connection key | Creates a key and drops it straight into the shown MCP command. | `Developer API console > MCP Connect > Generate connection key` (needs Enterprise plan) | - |
| Copy command | Copies the filled MCP command to the clipboard. | `Developer API console > MCP Connect > Copy command` (needs Enterprise plan) | - |
| Connector URL | Shows the plain MCP endpoint for clients that take a URL. | `Developer API console > MCP Connect` (needs Enterprise plan) | - |
| Client tabs | Switches the setup instructions between Claude, ChatGPT and Perplexity. | `Developer API console > MCP Connect` (needs Enterprise plan) | - |

#### Webhooks

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Destination URL | Sets the server Eto posts order events to. | `Developer API console > Webhooks` (needs Enterprise plan) | - |
| Send test | Fires a sample event at the URL in the field and shows the response. | `Developer API console > Webhooks > Send test` (needs Enterprise plan) | - |
| Deliver events to this URL | Pauses or resumes delivery without losing the configuration. | `Developer API console > Webhooks` (needs Enterprise plan) | - |
| Run test | Sends a chosen event for a chosen store to the saved URL, the internal test endpoint or a custom URL, and prints the full response. | `Developer API console > Webhooks > Run test` (needs Enterprise plan) | - |
| Internal test endpoint | Routes the test to a built-in inbox so a webhook can be tried without a public server. | `Developer API console > Webhooks > Send to > Internal test endpoint` (needs Enterprise plan) | - |
| Events | Chooses which Eto events are forwarded, with a live count. | `Developer API console > Webhooks > Events` (needs Enterprise plan) | - |
| Stores to forward | Picks which stores forward events, with search, a select-all/none/visible/invert menu and a Test selected button. | `Developer API console > Webhooks > Stores` (needs connected store) | - |
| Deliveries | Lists recent delivery attempts with status, and charts deliveries by day. | `Developer API console > Webhooks > Deliveries` (needs Enterprise plan) | `webhooks_deliveries` |
| Retry | Re-sends one failed delivery. | `Developer API console > Webhooks > Deliveries > Retry` (needs Enterprise plan) | `webhook_delivery_retry` |
| Signing secret | Shows the secret used to sign payloads, with copy and rotate. | `Developer API console > Webhooks` (needs Enterprise plan) | - |
| Save | Writes the URL, events, stores and active flag together. | `Developer API console > Webhooks > Save` (needs Enterprise plan) | - |
| Remove | Deletes the webhook configuration. | `Developer API console > Webhooks > Remove` (needs Enterprise plan) | - |

### Settings

- URL: https://eto.tools/settings/
- Anchor: https://eto.tools/dev/docs/#where-settings
- Markdown: https://eto.tools/dev/docs/where/settings.md
- Features: 46

Seven tabs holding account, billing, AI provider, notification, order integration, theme and affiliate settings.

#### Account

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Email | Shows the email the account signs in with. | `Settings > Account > Account` | - |
| Email confirmed | Shows whether the sign-in email has been verified. | `Settings > Account > Account` | - |
| Joined | Shows the date the account was created. | `Settings > Account > Account` | - |
| Update name | Opens a form that saves a new first and last name. | `Settings > Account > Account > Update name` | - |
| Change password | Opens a form that swaps the password after checking the current one. | `Settings > Account > Account > Change password` | - |
| Delete account | Arms a second confirm click that permanently deletes the account and all of its data. | `Settings > Account > Account > Delete account` | - |
| Time zone | Picks the zone that groups order dates and day totals on the Orders page, with a searchable list and a live clock. | `Settings > Account > Preferences > Time zone` | - |
| Automatic time zone | Follows the device time zone instead of a fixed one. | `Settings > Account > Preferences > Time zone > Automatic` | - |
| Download Eto Extension | Downloads the Chrome extension zip and shows the load-unpacked install steps. | `Settings > Account > Eto Extension > Manage` (needs extension feature) | - |
| Disconnect extension | Revokes the browser extension link to this account. | `Settings > Account > Eto Extension > Manage > Disconnect` (needs extension feature) | - |

#### Billing

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Free Trial card | Shows the trial state with start and end dates when a trial is running or available. | `Settings > Billing` | - |
| Start Free Trial | Sends an eligible account to onboarding to pick a plan and start the trial at no charge. | `Settings > Billing > Start Free Trial` | - |
| Subscription card | Shows plan name, status pill, billing cycle, renewal or cancellation date and the current period. | `Settings > Billing` | - |
| Refresh | Re-reads the subscription snapshot from Stripe. | `Settings > Billing > Refresh` | - |
| Daily Usage | Lists the daily caps and current counts for searches, bulk research, keyword analysis, connected stores, assistants and Shop Spy. | `Settings > Billing > Daily Usage` | - |
| Upgrade Plan | Sends the account to onboarding to change plan. | `Settings > Billing > Upgrade Plan` | - |
| Manage Billing | Opens the Stripe customer portal for invoices, card changes and cancellation. | `Settings > Billing > Manage Billing` (needs Stripe customer) | - |

#### AI

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Eto Credits | Uses the platform AI allowance, with a credits-this-month meter and reset date. | `Settings > AI > AI Provider > Eto Credits` (needs top-tier plan) | - |
| Your Vertex AI | Runs Gemini through your own GCP project using a project ID and service account JSON. | `Settings > AI > AI Provider > Your Vertex AI` | - |
| Gemini API Key | Runs Gemini on a Google AI Studio key billed to your own Google account. | `Settings > AI > AI Provider > Gemini API Key` | - |
| Provider status bar | States which provider is live, or warns that no provider is configured. | `Settings > AI > AI Provider` | - |
| Save provider | Saves the selected provider and its credentials. | `Settings > AI > AI Provider > Save provider` | - |

#### Notifications

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Play sale sound | Toggles the sound that plays when a new order is detected, saving on change. | `Settings > Notifications > Play sale sound` | - |
| Push notifications on your phone | Explains that phone alerts come through the Eto mobile app and links a request email. | `Settings > Notifications` | - |
| Test notification & sound | Fires a sample toast and plays the sale sound so both can be checked. | `Settings > Notifications > Test notification & sound` | - |
| Eto mobile app | A separate iOS client, requested from the owner here, that shows stores, orders and order breakdowns, finance figures and draft listings, and pushes a phone alert on a new order. | `Settings > Notifications > Contact the owner` | - |

#### Order Settings

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Printify API Token | Stores a Printify token so Printify orders and costs can be pulled in. | `Settings > Order Settings > Printify` | - |
| Toggle Printify token visibility | Shows or hides the stored Printify token in the field. | `Settings > Order Settings > Printify` | - |
| Shop | Picks which Printify shop the token should read, loaded from Printify after the token is saved. | `Settings > Order Settings > Printify > Shop` (needs Printify token) | - |
| Printify to Store Currency | Sets one exchange rate per non-USD store currency so Printify costs convert correctly. | `Settings > Order Settings > Currency Conversion` (needs connected store) | - |
| Save Rates | Saves the entered currency rates. | `Settings > Order Settings > Currency Conversion > Save Rates` (needs connected store) | - |
| Webhook URL | Shows the Eto endpoint Printify should post to, with a copy button. | `Settings > Order Settings > Webhooks > Copy URL` | - |
| Events to Track | Chooses which Printify events to subscribe to: order created, sent to production, shipment created and shipment delivered. | `Settings > Order Settings > Webhooks` | - |
| Enable Webhooks | Registers the selected Printify events against the webhook URL. | `Settings > Order Settings > Webhooks > Enable Webhooks` (needs Printify token) | - |
| Disable | Removes the registered Printify webhooks. | `Settings > Order Settings > Webhooks > Disable` (needs Printify webhooks enabled) | - |
| Recent Events | Lists the most recent Printify webhook deliveries once webhooks are on. | `Settings > Order Settings > Webhooks` (needs Printify webhooks enabled) | - |

#### Appearance

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Dark Mode colours | Sets the page background and sidebar colour used in dark mode, by picker or hex field. | `Settings > Appearance > Theme` | - |
| Light Mode colours | Sets the page background and sidebar colour used in light mode, by picker or hex field. | `Settings > Appearance > Theme` | - |
| Save | Stores the four custom colours in the browser and applies them across the app. | `Settings > Appearance > Theme > Save` | - |
| Reset | Clears the custom colours and puts both themes back to their defaults. | `Settings > Appearance > Theme > Reset` | - |

#### Affiliates

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Create Link | Claims a custom slug at eto.tools/af/ as your referral link. | `Settings > Affiliates > Create Link` | - |
| Copy | Copies the affiliate link to the clipboard. | `Settings > Affiliates > Copy` (needs affiliate link) | - |
| Affiliate stats | Shows headline referral numbers such as signups, conversions and earnings. | `Settings > Affiliates` (needs affiliate link) | - |
| Referred revenue | Charts the revenue generated by referred accounts over time. | `Settings > Affiliates` (needs affiliate link) | - |
| Your commission | Charts your commission over time. | `Settings > Affiliates` (needs affiliate link) | - |
| Recently Referred Users | Lists the latest accounts that signed up through your link with their status. | `Settings > Affiliates` (needs affiliate link) | - |

### Sidebar

- URL: https://eto.tools/dashboard/
- Anchor: https://eto.tools/dev/docs/#where-sidebar
- Markdown: https://eto.tools/dev/docs/where/sidebar.md
- Features: 38

The rail down the left of every dashboard page: navigation, the theme switch, and the quick panels that float research tools over whatever page you are on.

#### Sidebar navigation

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Structure mode | Shows the sidebar as sections: Finance, Listings, VA Management, Research, Developer, Settings and Admin. | `Sidebar > Structure` | - |
| Workflow mode | Shows the sidebar as numbered steps from Product Hunt through to Quick Upload, with a Unified Workflow link on top. | `Sidebar > Workflow` | - |
| Dashboard | Opens the main dashboard. | `Sidebar > Dashboard` | - |
| [Analytics](https://eto.tools/dashboard/analytics/) | Opens the analytics page. | `Sidebar > Analytics` | - |
| [Orders](https://eto.tools/dashboard/orders/) | Opens the orders page. | `Sidebar > Finance > Orders` | - |
| [Finance](https://eto.tools/dashboard/finances/) | Opens the finance page. | `Sidebar > Finance > Finance` | - |
| [Stores](https://eto.tools/dashboard/stores/) | Opens the connected stores page. | `Sidebar > Listings > Stores` (needs connect_stores feature) | - |
| [Listing Manager](https://eto.tools/dashboard/listings/) | Opens the listing manager. | `Sidebar > Listings > Listing Manager` (needs listing_manager feature) | - |
| [Quick upload](https://eto.tools/dashboard/single-research/) | Opens the quick upload workspace. | `Sidebar > Listings > Quick upload` (needs quick_upload feature) | - |
| [VA Admin](https://eto.tools/dashboard/va-admin/) | Opens virtual assistant management. | `Sidebar > VA Management > VA Admin` (needs va_management feature) | - |
| [Product Hunt](https://eto.tools/dashboard/etsy-search/) | Opens the Etsy product research page. | `Sidebar > Research > Product Hunt` (needs product_hunt feature) | - |
| [Keywords](https://eto.tools/dashboard/keywords/) | Opens keyword analysis. | `Sidebar > Research > Keywords` (needs staff) | - |
| [Shop Spy](https://eto.tools/dashboard/shop-spy/) | Opens the shop lookup tool. | `Sidebar > Research > Shop Spy` (needs shop_spy feature) | - |
| [Gallery](https://eto.tools/dashboard/gallery/) | Opens the AI image gallery. | `Sidebar > Research > Gallery` (needs ai_studio feature) | - |
| [API](https://eto.tools/dashboard/api/) | Opens the developer API console. | `Sidebar > Developer > API` (needs Enterprise plan) | - |
| [Settings](https://eto.tools/settings/) | Opens the settings page. | `Sidebar > Settings` | - |
| [Taxonomy Browser](https://eto.tools/dashboard/taxonomy/) | Opens the Etsy category browser. | `Sidebar > Admin > Taxonomy` (needs staff) | - |
| [Sign out](https://eto.tools/auth/logout/) | Signs you out and returns to the public site. | `Sidebar > Sign out` | - |
| [Locked nav item](https://eto.tools/onboarding/) | A feature outside the current plan shows in place but opens the upgrade flow instead of the page. | `Sidebar` | - |

#### Sidebar controls

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Collapse sidebar | Shrinks the sidebar to an icon rail and remembers the choice; it starts collapsed on narrow screens. | `Sidebar > Collapse sidebar` | - |
| Icon rail | The collapsed sidebar keeps every destination plus search, the three quick panels, theme and sign out as tooltipped icons. | `Sidebar > collapsed rail` | - |
| Theme | Switches between the light and dark theme and stores the choice in the browser. | `Sidebar > Theme` | - |
| Usage limits box | Shows the plan name and the remaining daily allowance for searches, bulk research, keyword analysis, stores, assistants and Shop Spy. | `Sidebar` | - |
| [Limit reached modal](https://eto.tools/onboarding/) | Blocks a capped feature, states the usage against the cap and offers Upgrade Plan. | `Sidebar` | - |
| New order toast | Shows a popup and plays the sale sound when a new order event arrives, deduplicated across tabs. | `Sidebar > new order toast` | - |

#### Quick panels

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Quick Thoughts | Opens a floating panel with Favourites, Thoughts and History tabs. | `Sidebar > Quick Thoughts` | - |
| Favourites | Lists saved listings with a search box, a refresh button and a sort menu covering demand, price and age in both directions. | `Sidebar > Quick Thoughts > Favourites` | - |
| Remove favourite | Drops one listing from the favourites list. | `Sidebar > Quick Thoughts > Favourites > Remove` | - |
| Thoughts | A free text notepad that saves itself as you type. | `Sidebar > Quick Thoughts > Thoughts` | - |
| History | Lists your recent searches so one can be reopened. | `Sidebar > Quick Thoughts > History` | - |
| Quick Keyword Search | Checks a keyword demand and competition in a floating panel, with mini metrics and a trend chart. | `Sidebar > Keyword Search` (needs plan tier 3+) | - |
| Quick Product Search | Looks up any Etsy listing by ID or pasted URL without leaving the page. | `Sidebar > Product Search` | - |
| Pretty / JSON | Switches the product lookup result between a readable card and the raw JSON. | `Sidebar > Product Search > Pretty` | - |
| Copy all tags | Copies every tag on the looked-up listing to the clipboard. | `Sidebar > Product Search > Copy all tags` | - |
| Drag to move | The title bar grip drags a quick panel anywhere on screen, clamped to the viewport. | `Sidebar > Quick Thoughts > drag handle` | - |
| Drag to resize | Every edge and corner of a quick panel is a resize grip. | `Sidebar > Quick Thoughts > corner grip` | - |
| Keyboard nudge | With a panel focused, arrow keys move it and Shift plus arrows resize it. | `Sidebar > Quick Thoughts` | - |
| Reset size and position | Puts a quick panel back to its default box. | `Sidebar > Quick Thoughts > Reset size and position` | - |

### Command palette

- URL: https://eto.tools/dashboard/
- Anchor: https://eto.tools/dev/docs/#where-command-palette
- Markdown: https://eto.tools/dev/docs/where/command-palette.md
- Features: 230

Cmd+K from anywhere: one box that jumps to a page, runs a control on the page you are on, or searches your listings, orders and stores.

#### Opening and searching

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Search or jump to... | Opens the command palette; Cmd+K or Ctrl+K opens it from any page. | `Command palette` | - |
| Fuzzy ranking | Matches whole words, prefixes, one-character typos and scattered letters, and boosts commands belonging to the page you are on or next to. | `Command palette` | - |
| Recents | Remembers the last eight commands you ran and floats them up the list. | `Command palette` | - |
| [Universal search](https://eto.tools/api/search/) | From two characters, searches your own listings, orders and stores on the server and folds the hits into the same list. | `Command palette` | - |
| Store-aware commands | Commands written for one store resolve to the store you last opened, so the label names it. | `Command palette` (needs connected store) | - |
| Run on arrival | Enter navigates to the destination page and then clicks, focuses or sets the control the command names. | `Command palette` | - |
| Open in a new tab | Cmd+Enter or Ctrl+Enter runs the highlighted command in a new tab. | `Command palette` | - |
| Keyboard navigation | Arrow keys move the highlight, Enter runs it and Escape closes the palette. | `Command palette` | - |

#### Pages

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Go to Dashboard | Jumps straight to the Dashboard page. | `Command palette > Go to Dashboard` | - |
| [Go to Analytics](https://eto.tools/dashboard/analytics/) | Jumps straight to the Analytics page. | `Command palette > Go to Analytics` | - |
| [Go to Orders](https://eto.tools/dashboard/orders/) | Jumps straight to the Orders page. | `Command palette > Go to Orders` | - |
| [Go to Finance](https://eto.tools/dashboard/finances/) | Jumps straight to the Finance page. | `Command palette > Go to Finance` | - |
| [Go to Stores](https://eto.tools/dashboard/stores/) | Jumps straight to the Stores page. | `Command palette > Go to Stores` | - |
| [Go to Listing Manager](https://eto.tools/dashboard/listings/) | Jumps straight to the Listing Manager page. | `Command palette > Go to Listing Manager` | - |
| [Go to Quick upload](https://eto.tools/dashboard/single-research/) | Jumps straight to the Quick upload page. | `Command palette > Go to Quick upload` | - |
| [Go to VA Admin](https://eto.tools/dashboard/va-admin/) | Jumps straight to the VA Admin page. | `Command palette > Go to VA Admin` | - |
| [Go to Product Hunt](https://eto.tools/dashboard/etsy-search/) | Jumps straight to the Product Hunt page. | `Command palette > Go to Product Hunt` | - |
| [Go to Keywords](https://eto.tools/dashboard/keywords/) | Jumps straight to the Keywords page. | `Command palette > Go to Keywords` | - |
| [Go to Gallery](https://eto.tools/dashboard/gallery/) | Jumps straight to the Gallery page. | `Command palette > Go to Gallery` | - |
| [Go to Shop Spy](https://eto.tools/dashboard/shop-spy/) | Jumps straight to the Shop Spy page. | `Command palette > Go to Shop Spy` | - |
| [Go to Settings](https://eto.tools/settings/) | Jumps straight to the Settings page. | `Command palette > Go to Settings` | - |
| [Go to Workflow](https://eto.tools/dashboard/workflow/) | Jumps straight to the Workflow page. | `Command palette > Go to Workflow` | - |
| [Go to API console](https://eto.tools/dashboard/api/) | Jumps straight to the API console page. | `Command palette > Go to API console` | - |
| [Go to Taxonomy browser](https://eto.tools/dashboard/taxonomy/) | Jumps straight to the Taxonomy browser page. | `Command palette > Go to Taxonomy browser` | - |
| [Go to Bulk research](https://eto.tools/dashboard/bulk-research/) | Jumps straight to the Bulk research page. | `Command palette > Go to Bulk research` | - |
| [Go to Customer support](https://eto.tools/dashboard/support/) | Jumps straight to the Customer support page. | `Command palette > Go to Customer support` | - |

#### Dashboard

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Home tab | Live updates, needs fulfilment, finance snapshot. | `Command palette > Home tab` | - |
| Issues tab | Stores that need attention. | `Command palette > Issues tab` | - |
| Listings tab | Active listings that have run out of stock. | `Command palette > Listings tab` | - |
| Switch store | Open the store switcher. | `Command palette > Switch store` | - |
| Dashboard · {store} | Show this store on the dashboard. | `Command palette > Dashboard · {store}` | - |
| All stores view | Combine every store on the dashboard. | `Command palette > All stores view` | - |
| All stores currency setup | Common currency and per-store rates. | `Command palette > All stores currency setup` | - |
| Out of stock listings | Per store, sorted by what they earned. | `Command palette > Listings > Out of stock listings` | - |
| Run store health check | Check every store connection with Etsy. | `Command palette > Issues > Run store health check` | - |
| Sort stores by most sales | Opens sort stores by most sales on Dashboard. | `Command palette > Store switcher > Sort stores by most sales` | - |
| Sort stores A to Z | Opens sort stores A to Z on Dashboard. | `Command palette > Store switcher > Sort stores A to Z` | - |
| Today | Sets the money window to today only. | `Command palette > Period > Today` | - |
| Yesterday | Sets the money window to yesterday only. | `Command palette > Period > Yesterday` | - |
| Last 7 days | Sets the money window to the last seven days. | `Command palette > Period > Last 7 days` | - |
| This month | Sets the money window to the current month so far. | `Command palette > Period > This month` | - |
| This year | Sets the money window to the current year so far. | `Command palette > Period > This year` | - |
| Custom date range | Pick two dates on the calendar. | `Command palette > Period > Custom date range` | - |
| Settled lens | Money on the day it moved. | `Command palette > Finance > Settled lens` | - |
| Outcome lens | What each day's sales turned out to be worth. | `Command palette > Finance > Outcome lens` | - |
| Orders chart | Switches the dashboard chart to the number of orders. | `Command palette > Finance > Orders chart` | - |
| Sales chart | Switches the dashboard chart to gross sales. | `Command palette > Finance > Sales chart` | - |
| COGS chart | Switches the dashboard chart to cost of goods. | `Command palette > Finance > COGS chart` | - |
| This store only (needs fulfilment) | Toggle the store filter on the live panel. | `Command palette > Live updates > This store only (needs fulfilment)` | - |
| Inactive stores (needs fulfilment) | Include orders from disconnected stores. | `Command palette > Live updates > Inactive stores (needs fulfilment)` | - |
| Clear live updates | Dismiss every order event. | `Command palette > Live updates > Clear live updates` | - |
| View all needing fulfilment | Unfulfilled physical orders. | `Command palette > Live updates > View all needing fulfilment` | - |

#### Analytics

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Analytics · Today](https://eto.tools/dashboard/analytics/) | Hour-by-hour for today. | `Command palette > Period > Analytics · Today` | - |
| [Analytics · Last 7 days](https://eto.tools/dashboard/analytics/) | Opens analytics · Last 7 days on Analytics. | `Command palette > Period > Analytics · Last 7 days` | - |
| [Analytics · Last 30 days](https://eto.tools/dashboard/analytics/) | Opens analytics · Last 30 days on Analytics. | `Command palette > Period > Analytics · Last 30 days` | - |
| [Analytics · Last 90 days](https://eto.tools/dashboard/analytics/) | Opens analytics · Last 90 days on Analytics. | `Command palette > Period > Analytics · Last 90 days` | - |
| [Analytics · Year to date](https://eto.tools/dashboard/analytics/) | Opens analytics · Year to date on Analytics. | `Command palette > Period > Analytics · Year to date` | - |
| [Analytics · {store}](https://eto.tools/dashboard/analytics/) | Show this store on Analytics. | `Command palette > Analytics · {store}` | - |
| [Where to sell next](https://eto.tools/dashboard/analytics/) | Ranked markets on the Orders map. | `Command palette > Where to sell next` | - |
| [Live orders on the map](https://eto.tools/dashboard/analytics/) | Watch orders land as they arrive. | `Command palette > Live orders on the map` | - |
| [Orders map · 3D globe](https://eto.tools/dashboard/analytics/) | Switches the orders map from flat to a 3D globe. | `Command palette > Orders map · 3D globe` | - |
| [Refresh analytics](https://eto.tools/dashboard/analytics/) | Recompute every panel now. | `Command palette > Refresh analytics` | - |

#### Orders

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Search orders](https://eto.tools/dashboard/orders/) | Order number, buyer, product, SKU. | `Command palette > Search orders` | - |
| [Orders · {store}](https://eto.tools/dashboard/orders/) | Only this store's orders. | `Command palette > Orders · {store}` | - |
| [Clear order filters](https://eto.tools/dashboard/orders/) | Clears every active order filter and shows the whole list again. | `Command palette > Clear order filters` | - |
| [Orders without COGS](https://eto.tools/dashboard/orders/) | Orders missing a cost of goods. | `Command palette > Orders without COGS` | - |
| [Sync orders from Etsy](https://eto.tools/dashboard/orders/) | Choose a range to pull. | `Command palette > Sync orders from Etsy` | - |
| [Sync Printify](https://eto.tools/dashboard/orders/) | Match Printify orders and pull tracking. | `Command palette > Sync Printify` | - |
| [Import COGs from CSV](https://eto.tools/dashboard/orders/) | Starts the four-step wizard that imports costs from a CSV file. | `Command palette > Import COGs from CSV` | - |
| [Share with supplier](https://eto.tools/dashboard/orders/) | Create a supplier link or download a CSV. | `Command palette > Share with supplier` | - |
| [Manage share links](https://eto.tools/dashboard/orders/) | Active and trashed supplier links. | `Command palette > Manage share links` | - |
| [Custom dates](https://eto.tools/dashboard/orders/) | Pick a date range on the calendar. | `Command palette > Date range > Custom dates` | - |

#### Finance

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Finance · {store}](https://eto.tools/dashboard/finances/) | Open this store in Finance. | `Command palette > Finance · {store}` | - |
| [All stores (Finance)](https://eto.tools/dashboard/finances/) | Combined view across every store. | `Command palette > All stores (Finance)` | - |
| [Settled lens](https://eto.tools/dashboard/finances/) | Money that moved, on the day it moved. | `Command palette > Settled lens` | - |
| [Outcome lens](https://eto.tools/dashboard/finances/) | What each day's sales turned out to be worth. | `Command palette > Outcome lens` | - |
| [Pick a day or range](https://eto.tools/dashboard/finances/) | Opens the calendar to choose one day or a range of days. | `Command palette > Pick a day or range` | - |
| [Copy view as JSON](https://eto.tools/dashboard/finances/) | Current figures to the clipboard. | `Command palette > Copy view as JSON` | - |
| [Verify with Etsy again](https://eto.tools/dashboard/finances/) | Re-runs the check that compares stored figures against Etsy. | `Command palette > Verify with Etsy again` | - |
| [Refresh today](https://eto.tools/dashboard/finances/) | Re-fetch today from Etsy. | `Command palette > Settings > Refresh today` | - |
| [Refresh previous 7 days](https://eto.tools/dashboard/finances/) | Opens refresh previous 7 days on Finance. | `Command palette > Settings > Refresh previous 7 days` | - |
| [Refresh previous 30 days](https://eto.tools/dashboard/finances/) | Opens refresh previous 30 days on Finance. | `Command palette > Settings > Refresh previous 30 days` | - |
| [Refresh a range of days](https://eto.tools/dashboard/finances/) | Pick the days to re-fetch. | `Command palette > Settings > Refresh a range of days` | - |
| [Adjust ad spend timing](https://eto.tools/dashboard/finances/) | Move ad charges to the day they were earned. | `Command palette > Settings > Adjust ad spend timing` | - |
| [Remove unpaid sales](https://eto.tools/dashboard/finances/) | Hide cancelled orders that were never paid. | `Command palette > Settings > Remove unpaid sales` | - |
| [Convert currency](https://eto.tools/dashboard/finances/) | Show every figure in another currency. | `Command palette > Settings > Convert currency` | - |
| [Currency settings](https://eto.tools/dashboard/finances/) | Exchange rates between your stores. | `Command palette > Settings > Currency settings` | - |
| [Manage subscriptions](https://eto.tools/dashboard/finances/) | Software you pay for outside Etsy. | `Command palette > Settings > Manage subscriptions` | - |
| [Set up skipped stores](https://eto.tools/dashboard/finances/) | Re-run finance setup for stores you skipped. | `Command palette > Settings > Set up skipped stores` | - |
| [Delete all finance data](https://eto.tools/dashboard/finances/) | Wipes every stored finance figure for the account and starts again. | `Command palette > Settings > Delete all finance data` | - |
| [Store exchange rates](https://eto.tools/dashboard/finances/) | Opens the tab holding the exchange rate between each store currency. | `Command palette > Currency settings > Store exchange rates` | - |
| [Export statement as CSV](https://eto.tools/dashboard/finances/) | Etsy statement layout for the selected window. | `Command palette > Export > Export statement as CSV` | - |
| [Export statement as Excel](https://eto.tools/dashboard/finances/) | .xlsx workbook, one sheet per store. | `Command palette > Export > Export statement as Excel` | - |
| [All stores tab](https://eto.tools/dashboard/finances/) | Combined breakdown (all stores view). | `Command palette > Breakdown > All stores tab` | - |
| [Per store tab](https://eto.tools/dashboard/finances/) | Every store side by side (all stores view). | `Command palette > Breakdown > Per store tab` | - |
| [Net profit breakdown](https://eto.tools/dashboard/finances/) | Opens net profit breakdown on Finance. | `Command palette > Breakdown > Net profit breakdown` | - |
| [Gross profit breakdown](https://eto.tools/dashboard/finances/) | Opens gross profit breakdown on Finance. | `Command palette > Breakdown > Gross profit breakdown` | - |
| [Sales breakdown](https://eto.tools/dashboard/finances/) | Opens sales breakdown on Finance. | `Command palette > Breakdown > Sales breakdown` | - |
| [Fees breakdown](https://eto.tools/dashboard/finances/) | Opens fees breakdown on Finance. | `Command palette > Breakdown > Fees breakdown` | - |
| [Ads breakdown](https://eto.tools/dashboard/finances/) | Opens ads breakdown on Finance. | `Command palette > Breakdown > Ads breakdown` | - |
| [Profit per product](https://eto.tools/dashboard/finances/) | Products view for the selected range. | `Command palette > Range > Profit per product` | - |
| [All transactions](https://eto.tools/dashboard/finances/) | Every group in the selected range. | `Command palette > Range > All transactions` | - |
| [Issues (unrecognised groups)](https://eto.tools/dashboard/finances/) | Ledger groups that could not be categorised. | `Command palette > Range > Issues (unrecognised groups)` | - |

#### Stores

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Connect a store](https://eto.tools/dashboard/stores/) | Starts the Etsy authorisation that connects a shop to Eto. | `Command palette > Connect a store` | - |
| [Refresh stores](https://eto.tools/dashboard/stores/) | Re-sync the store list from Etsy. | `Command palette > Refresh stores` | - |
| [Search stores](https://eto.tools/dashboard/stores/) | Opens search stores on Stores. | `Command palette > Search stores` | - |
| [Hidden stores](https://eto.tools/dashboard/stores/) | Lists the stores you have hidden from the dashboard, Orders or Finance. | `Command palette > Hidden stores` | - |
| [All stores (list)](https://eto.tools/dashboard/stores/) | Opens all stores (list) on Stores. | `Command palette > All stores (list)` | - |
| [Select all stores](https://eto.tools/dashboard/stores/) | Ticks every store in the list so a bulk action applies to all of them. | `Command palette > Select all stores` | - |
| [Store details · {store}](https://eto.tools/dashboard/stores/) | Stats, status, shipping profiles. | `Command palette > Store details · {store}` | - |
| [Refresh store details · {store}](https://eto.tools/dashboard/stores/) | Pulls this store details from Etsy again. | `Command palette > Refresh store details · {store}` | - |
| [Order notifications · {store}](https://eto.tools/dashboard/stores/) | Toggle webhook notifications for this store. | `Command palette > Order notifications · {store}` | - |
| [Disconnect · {store}](https://eto.tools/dashboard/stores/) | Remove this store from Eto. | `Command palette > Disconnect · {store}` | - |
| [Turn order notifications on for all stores](https://eto.tools/dashboard/stores/) | Opens turn order notifications on for all stores on Stores. | `Command palette > Bulk > Turn order notifications on for all stores` | - |
| [Turn order notifications off for all stores](https://eto.tools/dashboard/stores/) | Opens turn order notifications off for all stores on Stores. | `Command palette > Bulk > Turn order notifications off for all stores` | - |
| [Bulk visibility](https://eto.tools/dashboard/stores/) | Show or hide stores on Orders, Finance, Listings. | `Command palette > Bulk > Bulk visibility` | - |
| [Hide from Orders · {store}](https://eto.tools/dashboard/stores/) | Toggle this store on the Orders page. | `Command palette > Visibility > Hide from Orders · {store}` | - |
| [Hide from Finance · {store}](https://eto.tools/dashboard/stores/) | Toggle this store on the Finance page. | `Command palette > Visibility > Hide from Finance · {store}` | - |
| [Hide from Listings · {store}](https://eto.tools/dashboard/stores/) | Toggle this store in the Listing Manager. | `Command palette > Visibility > Hide from Listings · {store}` | - |

#### Store details

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Shop information · {store}](https://eto.tools/dashboard/stores/) | Currency, location, languages, created. | `Command palette > Shop information · {store}` | - |
| [Shop status · {store}](https://eto.tools/dashboard/stores/) | Vacation, custom requests, payments. | `Command palette > Shop status · {store}` | - |
| [Shipping profiles · {store}](https://eto.tools/dashboard/stores/) | Opens shipping profiles · {store} on Stores. | `Command palette > Shipping profiles · {store}` | - |
| [Shop announcement · {store}](https://eto.tools/dashboard/stores/) | Opens shop announcement · {store} on Stores. | `Command palette > Shop announcement · {store}` | - |

#### Listing Manager

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Listings · {store}](https://eto.tools/dashboard/listings/) | Open this store in the Listing Manager. | `Command palette > Listings · {store}` | - |
| [Grid view](https://eto.tools/dashboard/listings/) | Opens grid view on Listing Manager. | `Command palette > Grid view` | - |
| [List view](https://eto.tools/dashboard/listings/) | Opens list view on Listing Manager. | `Command palette > List view` | - |
| [Grid columns](https://eto.tools/dashboard/listings/) | Sets how many cards fit across one row of the grid. | `Command palette > Grid columns` | - |
| [Search listings](https://eto.tools/dashboard/listings/) | Opens search listings on Listing Manager. | `Command palette > Search listings` | - |
| [Refresh this store's listings](https://eto.tools/dashboard/listings/) | Opens refresh this store's listings on Listing Manager. | `Command palette > Refresh this store's listings` | - |
| [Refresh all stores' listings](https://eto.tools/dashboard/listings/) | Opens refresh all stores' listings on Listing Manager. | `Command palette > Refresh all stores' listings` | - |
| [Default mode](https://eto.tools/dashboard/listings/) | Opens default mode on Listing Manager. | `Command palette > Mode > Default mode` | - |
| [Edit products mode](https://eto.tools/dashboard/listings/) | Select listings for bulk actions. | `Command palette > Mode > Edit products mode` | - |
| [Edit storewide mode](https://eto.tools/dashboard/listings/) | Rules that apply to whole stores. | `Command palette > Mode > Edit storewide mode` | - |
| [All listings](https://eto.tools/dashboard/listings/) | Opens all listings on Listing Manager. | `Command palette > Status > All listings` | - |
| [Active listings](https://eto.tools/dashboard/listings/) | Opens active listings on Listing Manager. | `Command palette > Status > Active listings` | - |
| [Inactive listings](https://eto.tools/dashboard/listings/) | Opens inactive listings on Listing Manager. | `Command palette > Status > Inactive listings` | - |
| [Draft listings](https://eto.tools/dashboard/listings/) | Opens draft listings on Listing Manager. | `Command palette > Status > Draft listings` | - |
| [Filter by shop section](https://eto.tools/dashboard/listings/) | Opens filter by shop section on Listing Manager. | `Command palette > Filters > Filter by shop section` | - |
| [Filter by type](https://eto.tools/dashboard/listings/) | Narrows the listings to digital or physical products. | `Command palette > Filters > Filter by type` | - |
| [Sort listings](https://eto.tools/dashboard/listings/) | Quantity, views, favourites, sold, price, age. | `Command palette > Filters > Sort listings` | - |
| [Filter by age](https://eto.tools/dashboard/listings/) | Listings created within a period. | `Command palette > Filters > Filter by age` | - |
| [Select all listings](https://eto.tools/dashboard/listings/) | Opens select all listings on Listing Manager. | `Command palette > Bulk > Select all listings` | - |
| [Select all drafts](https://eto.tools/dashboard/listings/) | Opens select all drafts on Listing Manager. | `Command palette > Bulk > Select all drafts` | - |
| [Select all active](https://eto.tools/dashboard/listings/) | Opens select all active on Listing Manager. | `Command palette > Bulk > Select all active` | - |
| [Select all inactive](https://eto.tools/dashboard/listings/) | Opens select all inactive on Listing Manager. | `Command palette > Bulk > Select all inactive` | - |
| [Edit processing profiles](https://eto.tools/dashboard/listings/) | Production partners for the selected listings. | `Command palette > Bulk > Edit processing profiles` | - |
| [Edit quantity](https://eto.tools/dashboard/listings/) | Set stock for the selected listings. | `Command palette > Bulk > Edit quantity` | - |
| [Copy listings to another store](https://eto.tools/dashboard/listings/) | Opens copy listings to another store on Listing Manager. | `Command palette > Bulk > Copy listings to another store` | - |
| [Activate selected on Etsy](https://eto.tools/dashboard/listings/) | Publish drafts ($0.20 each). | `Command palette > Bulk > Activate selected on Etsy` | - |
| [Delete selected from Etsy](https://eto.tools/dashboard/listings/) | Opens delete selected from Etsy on Listing Manager. | `Command palette > Bulk > Delete selected from Etsy` | - |
| [Auto quantity control](https://eto.tools/dashboard/listings/) | Keep stock topped up automatically. | `Command palette > Storewide > Auto quantity control` | - |

#### Quick upload

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Active listings (uploads)](https://eto.tools/dashboard/single-research/) | Listings already live. | `Command palette > Active listings (uploads)` | - |
| [Drafts](https://eto.tools/dashboard/single-research/) | Listings not yet published. | `Command palette > Drafts` | - |
| [Scheduled uploads](https://eto.tools/dashboard/single-research/) | Listings queued to publish later. | `Command palette > Scheduled uploads` | - |
| [New listing](https://eto.tools/dashboard/single-research/) | Creates an empty listing and opens the workspace on it. | `Command palette > New listing` | - |
| [Bulk CSV import](https://eto.tools/dashboard/single-research/) | Create many listings from a spreadsheet. | `Command palette > Bulk CSV import` | - |
| [Delete selected uploads](https://eto.tools/dashboard/single-research/) | Opens delete selected uploads on Quick upload. | `Command palette > Delete selected uploads` | - |
| [Import an Etsy listing by ID](https://eto.tools/dashboard/single-research/) | Start from an existing listing. | `Command palette > New > Import an Etsy listing by ID` | - |
| [Start a blank digital listing](https://eto.tools/dashboard/single-research/) | Opens start a blank digital listing on Quick upload. | `Command palette > New > Start a blank digital listing` | - |
| [Start a blank physical listing](https://eto.tools/dashboard/single-research/) | Opens start a blank physical listing on Quick upload. | `Command palette > New > Start a blank physical listing` | - |

#### Listing workspace

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Generate title with AI](https://eto.tools/dashboard/single-research/) | Opens generate title with AI on Quick upload. | `Command palette > Generate title with AI` | - |
| [Generate description with AI](https://eto.tools/dashboard/single-research/) | Opens generate description with AI on Quick upload. | `Command palette > Generate description with AI` | - |
| [Add images](https://eto.tools/dashboard/single-research/) | Opens add images on Quick upload. | `Command palette > Add images` | - |
| [Choose category](https://eto.tools/dashboard/single-research/) | Picks the Etsy category the listing belongs to. | `Command palette > Choose category` | - |
| [Choose target stores](https://eto.tools/dashboard/single-research/) | Opens choose target stores on Quick upload. | `Command palette > Choose target stores` | - |
| [About section](https://eto.tools/dashboard/single-research/) | Title, description, media. | `Command palette > About section` | - |
| [Price & inventory section](https://eto.tools/dashboard/single-research/) | Opens price & inventory section on Quick upload. | `Command palette > Price & inventory section` | - |
| [Details section](https://eto.tools/dashboard/single-research/) | Category, tags, materials, stores. | `Command palette > Details section` | - |
| [Publish listing](https://eto.tools/dashboard/single-research/) | Opens publish listing on Quick upload. | `Command palette > Publish listing` | - |
| [Schedule publish](https://eto.tools/dashboard/single-research/) | Opens schedule publish on Quick upload. | `Command palette > Schedule publish` | - |
| [Bulk upload variants](https://eto.tools/dashboard/single-research/) | Generate many variants of this listing. | `Command palette > Bulk upload variants` | - |

#### VA Admin

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Add a virtual assistant](https://eto.tools/dashboard/va-admin/) | Opens add a virtual assistant on VA Admin. | `Command palette > Add a virtual assistant` | - |
| [Search assistants](https://eto.tools/dashboard/va-admin/) | Opens search assistants on VA Admin. | `Command palette > Search assistants` | - |
| [Assistant details](https://eto.tools/dashboard/va-admin/) | Opens assistant details on VA Admin. | `Command palette > Edit > Assistant details` | - |
| [Assistant activity](https://eto.tools/dashboard/va-admin/) | Logins, searches, listings, fulfilments. | `Command palette > Edit > Assistant activity` | - |
| [Toggle assistant active](https://eto.tools/dashboard/va-admin/) | Opens toggle assistant active on VA Admin. | `Command palette > Edit > Toggle assistant active` | - |
| [Delete this assistant](https://eto.tools/dashboard/va-admin/) | Opens delete this assistant on VA Admin. | `Command palette > Edit > Delete this assistant` | - |
| [Order permissions](https://eto.tools/dashboard/va-admin/) | Which stores this assistant can handle orders for. | `Command palette > Permissions > Order permissions` | - |
| [Upload permissions](https://eto.tools/dashboard/va-admin/) | Which stores this assistant can upload to. | `Command palette > Permissions > Upload permissions` | - |
| [Enable all stores](https://eto.tools/dashboard/va-admin/) | Opens enable all stores on VA Admin. | `Command palette > Permissions > Enable all stores` | - |
| [Disable all stores](https://eto.tools/dashboard/va-admin/) | Opens disable all stores on VA Admin. | `Command palette > Permissions > Disable all stores` | - |
| [Save permissions](https://eto.tools/dashboard/va-admin/) | Opens save permissions on VA Admin. | `Command palette > Permissions > Save permissions` | - |

#### Product Hunt

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Hunt for products](https://eto.tools/dashboard/etsy-search/) | Type a keyword and search Etsy. | `Command palette > Hunt for products` | - |
| [Run the search](https://eto.tools/dashboard/etsy-search/) | Opens run the search on Product Hunt. | `Command palette > Run the search` | - |
| [Search settings](https://eto.tools/dashboard/etsy-search/) | Amount, age, detail level, optimized mode. | `Command palette > Search settings` | - |
| [Grid columns](https://eto.tools/dashboard/etsy-search/) | Sets how many cards fit across one row of the grid. | `Command palette > Grid columns` | - |
| [AI Gems](https://eto.tools/dashboard/etsy-search/) | AI-picked low-competition winners. | `Command palette > AI Gems` | - |
| [Keyword analysis](https://eto.tools/dashboard/etsy-search/) | Tags and keywords across the results. | `Command palette > Keyword analysis` | - |
| [Next page of results](https://eto.tools/dashboard/etsy-search/) | Opens next page of results on Product Hunt. | `Command palette > Next page of results` | - |
| [Previous page of results](https://eto.tools/dashboard/etsy-search/) | Opens previous page of results on Product Hunt. | `Command palette > Previous page of results` | - |
| [Amount of products](https://eto.tools/dashboard/etsy-search/) | How many listings to scan. | `Command palette > Settings > Amount of products` | - |
| [Max listing age](https://eto.tools/dashboard/etsy-search/) | Only listings younger than N months. | `Command palette > Settings > Max listing age` | - |
| [Full product details](https://eto.tools/dashboard/etsy-search/) | Fetches the full detail of every listing found, which is slower but complete. | `Command palette > Settings > Full product details` | - |
| [Optimized mode](https://eto.tools/dashboard/etsy-search/) | AI filtering and ranking. | `Command palette > Settings > Optimized mode` | - |
| [Sort results](https://eto.tools/dashboard/etsy-search/) | Opens sort results on Product Hunt. | `Command palette > Filters > Sort results` | - |
| [Filter by listing type](https://eto.tools/dashboard/etsy-search/) | Narrows the results to physical or digital listings. | `Command palette > Filters > Filter by listing type` | - |
| [Filter by variations](https://eto.tools/dashboard/etsy-search/) | Opens filter by variations on Product Hunt. | `Command palette > Filters > Filter by variations` | - |
| [Filter by ships-from country](https://eto.tools/dashboard/etsy-search/) | Opens filter by ships-from country on Product Hunt. | `Command palette > Filters > Filter by ships-from country` | - |
| [Search within results](https://eto.tools/dashboard/etsy-search/) | Opens search within results on Product Hunt. | `Command palette > Filters > Search within results` | - |

#### Settings

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [Account](https://eto.tools/settings/) | Profile, security, time zone. | `Command palette > Account` | - |
| [Billing](https://eto.tools/settings/) | Plan and subscription (opens the Stripe portal on paid plans). | `Command palette > Billing` | - |
| [AI provider](https://eto.tools/settings/) | Eto credits, Vertex AI or your own Gemini key. | `Command palette > AI provider` | - |
| [Notifications](https://eto.tools/settings/) | Opens notifications on Settings. | `Command palette > Notifications` | - |
| [Order settings](https://eto.tools/settings/) | Printify, currency rates, webhooks. | `Command palette > Order settings` | - |
| [Appearance](https://eto.tools/settings/) | Switches the app between the light and dark theme. | `Command palette > Appearance` | - |
| [Affiliates](https://eto.tools/settings/) | Your referral link and commissions. | `Command palette > Affiliates` | - |
| [Update name](https://eto.tools/settings/) | Opens update name on Settings. | `Command palette > Account > Update name` | - |
| [Change password](https://eto.tools/settings/) | Opens change password on Settings. | `Command palette > Account > Change password` | - |
| [Time zone](https://eto.tools/settings/) | How order dates and day totals are grouped. | `Command palette > Account > Time zone` | - |
| [Eto browser extension](https://eto.tools/settings/) | Opens eto browser extension on Settings. | `Command palette > Account > Eto browser extension` | - |
| [Delete account](https://eto.tools/settings/) | Starts the permanent account deletion, which cannot be undone. | `Command palette > Account > Delete account` | - |
| [Daily usage limits](https://eto.tools/settings/) | Opens daily usage limits on Settings. | `Command palette > Billing > Daily usage limits` | - |
| [Manage billing](https://eto.tools/settings/) | Opens the Stripe portal to change the card, plan or invoices. | `Command palette > Billing > Manage billing` | - |
| [Upgrade plan](https://eto.tools/settings/) | Opens upgrade plan on Settings. | `Command palette > Billing > Upgrade plan` | - |
| [Play sale sound](https://eto.tools/settings/) | Turns the sale sound on a new order on or off. | `Command palette > Notifications > Play sale sound` | - |
| [Test notification and sound](https://eto.tools/settings/) | Opens test notification and sound on Settings. | `Command palette > Notifications > Test notification and sound` | - |
| [Printify API token](https://eto.tools/settings/) | Opens printify API token on Settings. | `Command palette > Orders > Printify API token` | - |
| [Currency rates for order costs](https://eto.tools/settings/) | Opens currency rates for order costs on Settings. | `Command palette > Orders > Currency rates for order costs` | - |
| [Printify webhooks](https://eto.tools/settings/) | Order and shipment events. | `Command palette > Orders > Printify webhooks` | - |
| [Create affiliate link](https://eto.tools/settings/) | Opens create affiliate link on Settings. | `Command palette > Affiliates > Create affiliate link` | - |

#### API console

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| [API keys](https://eto.tools/dashboard/api/) | Opens the panel that creates, rotates and revokes API keys. | `Command palette > API keys` | - |
| [MCP connect](https://eto.tools/dashboard/api/) | Connect Claude or another MCP client. | `Command palette > MCP connect` | - |
| [API webhooks](https://eto.tools/dashboard/api/) | Opens the panel that points Eto order events at your own server. | `Command palette > API webhooks` | - |

#### Sidebar

| Feature | What it does | Where | API |
| --- | --- | --- | --- |
| Quick thoughts | Favourites, notes and search history. | `Command palette > Quick thoughts` | - |
| Quick keyword search | Check a keyword without leaving the page. | `Command palette > Quick keyword search` | - |
| Quick product search | Opens the quick product search panel over the current page. | `Command palette > Quick product search` | - |
| Toggle dark mode | Opens toggle dark mode on the app. | `Command palette > Toggle dark mode` | - |
| Collapse or expand the sidebar | Opens collapse or expand the sidebar on the app. | `Command palette > Collapse or expand the sidebar` | - |
| Workflow mode (sidebar) | Show the sidebar as numbered steps. | `Command palette > Workflow mode (sidebar)` | - |
| Structure mode (sidebar) | Show the sidebar as sections. | `Command palette > Structure mode (sidebar)` | - |
