# 1688 Supplier Finder — Bulk Supplier Shortlist & Comparison (`crawleast/1688-supplier-finder`) Actor

Compare 1688 suppliers across your whole product list: a ranked, deduplicated shortlist per product with prices, MOQ and estimated landed cost, plus a free rollup of the suppliers that cover several of your SKUs.

- **URL**: https://apify.com/crawleast/1688-supplier-finder.md
- **Developed by:** [Kyle Wang](https://apify.com/crawleast) (community)
- **Categories:** E-commerce, Lead generation, Agents
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 delivered supplier shortlist rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 1688 Supplier Finder — Bulk Supplier Shortlist & Comparison

> **Send your product list. See which 1688 suppliers can cover several of your products, plus a short, ranked supplier shortlist for every product.**

Built for **multi-SKU sourcing**: compare 1688 suppliers across a whole product catalog in one run. Send one product photo URL per product, as JSON or as a pasted CSV with your SKUs and ASINs. One run returns:

| Output | What it answers | Cost |
| --- | --- | --- |
| **Supplier rollup**: `SUPPLIER_ROLLUP` (JSON + CSV) | Which suppliers show up for several of my products? Can I consolidate my catalog into fewer factories? | Free |
| **Shortlist rows**: the dataset | For each product, the top offers (5 by default), deduplicated where company identity is known, ranked by estimated landed cost | Pay per event |
| **Input status**: `INPUT_STATUS` (JSON + CSV) | What happened to each product I sent? | Free |

**Why the rollup comes first:** buyers with a full catalog usually prefer a few stable suppliers to the cheapest offer per SKU. Fewer suppliers mean fewer relationships to manage, fewer sample rounds and simpler consolidated shipments. Every row carries `supplierCatalogCoverage` (how many of *your* products that supplier appears for), and the rollup lists suppliers by how many of your products they matched.

**Pricing (FREE plan):** $0.010 per matched product + $0.003 per row + $0.002 per enriched row, plus $0.01 once per run that delivers rows. 10 products × 5 offers with 1 enriched each = **$0.28**. Paid plans discount the three main events by 10–30%; the base fee stays $0.01. All examples below use FREE-plan prices. Failed, unmatched and skipped products cost nothing; there is no fee just for starting a run.

**What the fee replaces:** shortlisting 5 suppliers for each of 100 products by hand means opening, translating and costing about 500 listings. One run returns the ranked shortlists and the shared-supplier rollup for $2.71 (FREE plan, 1 enriched offer per product).

**One product or a whole list?** To look up a single product and see every raw candidate, use [1688 Image Search](https://apify.com/crawleast/1688-image-search-scraper) (up to 20 cards per image). Use this Actor when you need a decision across many products: a ranked shortlist per product and the suppliers they share.

### Fewer rows, each ready for a decision

This Actor deliberately returns fewer rows than a raw search. Per product you get a short list (default 5) that is already:

- **Deduplicated where corporate identity is available**: one offer per identified company per product (`supplierDedupe`). Offers without a reliable company ID cannot be deduplicated by supplier.
- **Ranked**: by estimated landed cost (default), unit price, supplier years, match position or supplier coverage; `rankBasis` records which key placed each row.
- **Costed**: the top offer of each product (`enrichDepth: 1`) gets a full product-detail fetch with price ladder, confirmed price, landed-cost and margin estimates and a dropship score, wherever 1688 provides the data.
- **Flat**: one row per product × offer, carrying your `clientSku` and `inputAsin`, ready for spreadsheets, BI tools and joins.
- **Rolled up**: suppliers grouped across your whole list, free of charge.

### Quick start

**Photos as JSON** (`products`):

```json
{
  "products": [
    {
      "imageUrl": "https://cbu01.alicdn.com/img/ibank/15418323391_1847764777.jpg",
      "clientSku": "SKU-001",
      "asin": "B0EXAMPLE1"
    },
    {
      "imageUrl": "https://cbu01.alicdn.com/img/ibank/O1CN01FCwJrl29btzNvQkHM_!!2235628087-0-cib.jpg_270x270xzq60.jpg",
      "clientSku": "SKU-002"
    }
  ],
  "shortlistSize": 5,
  "enrichDepth": 1,
  "sortBy": "landedCost",
  "targetMarket": "US"
}
```

If both photos match and each fills its shortlist: $0.01 + 2 × $0.010 + 10 × $0.003 + 2 × $0.002 = **$0.064**.

**A pasted CSV** (`productsCsv`, header row required):

```csv
sku,asin,image_url,title
SKU-001,B0EXAMPLE1,https://cbu01.alicdn.com/img/ibank/15418323391_1847764777.jpg,
SKU-002,,https://cbu01.alicdn.com/img/ibank/O1CN01FCwJrl29btzNvQkHM_!!2235628087-0-cib.jpg_270x270xzq60.jpg,
```

Every data row needs a photo URL; `title` is only an optional reference label, not a search input. All other settings keep their defaults (5 offers per product, `enrichDepth` 1, ranked by landed cost, US market). If both rows match and fill their shortlists, the run costs **$0.064**. Through the API, send the same text as one JSON string with `\n` line breaks.

When the run finishes, the **Output** tab links the rows, `SUPPLIER_ROLLUP.csv` and `INPUT_STATUS.csv`.

### Recommended setups

Every recommended setup uses `enrichDepth: 1`: the top offer of each product gets a full product detail (price ladder, confirmed price and, where available, a landed-cost estimate) for $0.002, and the other rows stay list-level.

| Goal | Send | Settings |
| --- | --- | --- |
| Amazon FBA / private label sourcing | Photos of products you plan to sell (your own shot or a listing's main image), with SKU and ASIN as labels | `shortlistSize: 5`, `enrichDepth: 1`, `sortBy: "landedCost"`, `targetMarket` = your marketplace |
| Consolidate a catalog into fewer suppliers | Your catalog as a CSV with image URLs | `shortlistSize: 5`, `enrichDepth: 1`, `sortBy: "supplierCoverage"`, then read `SUPPLIER_ROLLUP.csv` from the top |
| Dropshipping / multi-store product sourcing | A store export with image URLs | `shortlistSize: 5`, `enrichDepth: 1`, `sortBy: "landedCost"` |
| Brand looking for established manufacturers | Product photos | `shortlistSize: 5`, `enrichDepth: 1`, `sortBy: "supplierYears"`, then filter on `isFactory` |
| Automation or AI-agent pipeline | One API call per batch, on a schedule | Any of the above, with spend capped by `maxTotalChargeUsd` |

Tips: product-only photos on a plain background match best. Put suppliers you have already rejected into `excludeSupplierIds`.

### How it works

1. **Match.** Each photo goes through 1688's reverse image search. Up to `maxResultsPerProduct` candidates (default 20) are collected per product. An optional input title is copied back as a label; title-only searching is not supported.
2. **Filter and count coverage.** Suppliers in `excludeSupplierIds` and candidates below `minMatchScore` are removed. For every identified company, the Actor then counts how many of your products it appears for (`supplierCatalogCoverage`).
3. **Shortlist.** Remaining candidates are sorted by `sortBy` (landed cost is not known yet, so `landedCost` sorts by unit price). Each identified company keeps its best offer per product, and the top `shortlistSize` remain.
4. **Enrich.** The first `enrichDepth` offers of each shortlist get a full product-detail fetch.
5. **Final rank.** With `sortBy: "landedCost"`, rows with a landed-cost estimate come first (cheapest first) and the rest follow by unit price. `rank` runs from 1 to `shortlistSize`; `rankBasis` records the key used.
6. **Deliver.** Rows go to the dataset; the rollup and the status list go to the key-value store.

### Input

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `products` | array | — | Items like `{"imageUrl": "...", "clientSku": "...", "asin": "..."}`. Each requires a public http(s) photo URL without credentials. `title`, `clientSku` and `asin` are optional reference labels; title-only entries are not searched. |
| `productsCsv` | string | — | Alternative to `products`: raw CSV text with an image URL header (up to 2 MB and 100 data rows). If both are sent, `products` is used. |
| `shortlistSize` | integer | `5` | Offers returned per product (1–10). |
| `enrichDepth` | integer | `1` (from `enrichDetails`) | How many of each product's top offers get a full detail fetch (0 to `shortlistSize`). |
| `enrichDetails` | boolean | `true` | Used only when `enrichDepth` is not set: `true` gives 1, `false` gives 0. |
| `maxResultsPerProduct` | integer | `20` | Candidate pool per product before filtering and ranking (1–20). |
| `targetMarket` | enum | `US` | `US`, `EU`, `UK`, `AU` or `global`. Drives duty estimates and certification hints. |
| `sortBy` | enum | `landedCost` | `landedCost`, `price`, `supplierYears`, `matchScore` or `supplierCoverage`. With `enrichDepth: 0`, `landedCost` falls back to unit price. |
| `minMatchScore` | number | `0` | Drop candidates below this relative score (0–1). `0` ranks without cutting anything. |
| `supplierDedupe` | boolean | `true` | Keep one offer per supplier per product. |
| `excludeSupplierIds` | array | `[]` | Supplier `companyId` values to leave out. |
| `maxTotalChargeUsd` | number | `20` | Spending cap for the run (minimum 0.04). See [Pricing](#pricing-pay-per-event). |

Over-long values are trimmed rather than rejected: titles to 200 characters, `asin` to 32, `clientSku` to 64.

### Output: one flat row per product × offer

Each dataset item pairs one of your input products with one 1688 offer. Products that fail or don't match produce no rows. Every field is typed and described in the dataset schema.

**Your input, echoed**

| Field | Meaning |
| --- | --- |
| `inputIndex` | 0-based position in your list (the first CSV data row is 0) |
| `clientSku` | Your SKU, unchanged |
| `inputAsin` | Your ASIN label, unchanged; it is never looked up |
| `inputType` | `image` for supported inputs |
| `inputImageUrl`, `inputTitle` | The photo URL and title you sent |

**Match**

| Field | Meaning |
| --- | --- |
| `matchScore` | Relative score from 1688's result position: 1.0 = first result (see FAQ) |
| `rank` | Position in this product's shortlist (1 = best by your `sortBy`) |
| `rankBasis` | The key that placed the row: `landedCost`, `price`, `supplierYears`, `matchScore` or `supplierCoverage` |
| `matchMethod` | `image` (reverse image search) |
| `enriched` | `true` when a product-detail page was fetched and delivered for this row; `false` for list-level rows |

**Offer and price**

| Field | Meaning |
| --- | --- |
| `offerId`, `offerUrl` | 1688 offer ID and listing link |
| `titleCN` | Original Chinese title (authoritative) |
| `titleEN` | Best-effort English machine translation |
| `mainImage` | Listing main image |
| `categoryId` | 1688 category ID, when the search result carries one |
| `unitPriceCNY`, `unitPriceUSD` | Unit price for the product's MOQ tier: the lowest specification price at the highest quantity threshold reachable at `moq`. This is not the cheapest bulk-quantity price across all breaks; larger-quantity tiers stay in `priceLadder`. USD uses a fixed CNY/USD rate |
| `priceBasis` | Where the unit price came from: `current_price_table_moq` uses the MOQ-applicable tier, `promotion_min` and `base_offer_min` use the source range minimum, `search_card` is unverified list data, `detail_minimum` when the exact source path is unavailable |
| `priceEvidenceKey` | Free key-value record with the captured source price, MOQ, SKU/spec and capture time for audit. Null on list rows or if it could not be saved |
| `moq` | Minimum order quantity |
| `priceLadder` | Published detail-price entries with quantity thresholds where supplied (enriched rows only). Equal thresholds can describe different specifications, not quantity discounts; a missing threshold stays null. |

**Supplier**

| Field | Meaning |
| --- | --- |
| `companyId` | 1688 company ID, or `name:<registered company name>` when available without a member ID; see `companyIdSource`. Used for dedupe, exclusions and rollup. Individual seller IDs and unverified keyword storefront labels are not published as company identities. |
| `companyName` | Company name (in Chinese); blank for individual sellers. Storefront labels are not treated as independently verified registered identity. |
| `yearsOnPlatform` | Years on 1688 |
| `verifiedFlag` | `true` when 1688 shows a factory-inspection or Super Factory badge |
| `isFactory`, `isSuperFactory` | 1688 search-card factory flag and Super Factory badge. `bizType: "Manufacturer"` is a separate business-model label and does not force `isFactory` to `true` |
| `province`, `city` | Supplier location |
| `supplierUrl` | Supplier's 1688 storefront when available; blank for individual sellers |
| `supplierCatalogCoverage` | How many of your products this supplier appears for in this run (counted over each product's candidate pool, after exclusions and minimum-match filtering) |

**Decision and compliance fields (enriched rows only)**

| Field | Meaning |
| --- | --- |
| `landedCostUSD` | Planning estimate per unit delivered to `targetMarket`: lowest published purchase price plus shipping, duty and fee estimates. It is not a quote for the variant, pack or quantity you choose |
| `landedCostBasis` | `matched_sku` when the minimum price and weight come from the same explicit source SKU; otherwise `minimum_price_representative_weight` or `minimum_price_inferred_weight` marks a rough estimate |
| `landedCostWeightKg` | Weight actually used by the cost estimate, including a price-based guess when no source weight exists |
| `unitWeightKg` | Source weight from the detail page. It is the linked SKU weight when unambiguous, otherwise a representative SKU average; null when no source weight exists |
| `unitWeightBasis` | `matched_sku`, `representative_sku_average` or `price_inferred` |
| `costSkuId`, `costSpec` | Source SKU and sanitized specification used for a `matched_sku` estimate; null when no single SKU can be proved |
| `suggestedRetailUSD` | Landed cost × a rule-of-thumb markup, not a market price check |
| `estimatedMarginPct` | Margin implied by `suggestedRetailUSD`: a planning figure, not a profit forecast |
| `dropshipScore` | Dropship-readiness heuristic from MOQ, dropship support and supplier signals (higher is better) |
| `complianceFlags` | Keyword-based risk hints (e.g. electronics and batteries, food, infant toys) |
| `suggestedCertifications` | Certifications often needed in `targetMarket` for this kind of product: hints, not legal advice |

**Trust**

| Field | Meaning |
| --- | --- |
| `priceVerified` | `true` when a positive price was read from the captured detail payload. It does not prove the selected variant, current checkout price or product match; use `priceEvidenceKey` and `scrapedAt` for audit |
| `detailLevel` | `list` (search data only), `detail` (detail page with price, price ladder and weight) or `partial` (detail page delivered, but the price, ladder or weight is missing) |
| `scrapedAt` | When the row was read; on evidence-linked enriched rows, the detail payload capture time (ISO 8601) |
| `sourceUrl` | The 1688 offer page associated with the candidate or fetched detail |

Rows that were not enriched leave the decision fields, compliance hints and `priceLadder` empty **by design** (see FAQ). The dataset also carries helper fields such as `companyIdSource`, `factoryInspected`, `bizType`, `individualSeller`, `translationMethod`, `originalPriceCNY`, `hasDiscount`, `dropshipGrade` and `dataQuality`; the dataset schema describes them all.

#### Price evidence: `PRICE_EVIDENCE_<offerId>` (free)

Each successfully fetched detail offer gets one JSON record in the run's key-value store. It keeps the captured price range, quantity thresholds, SKU IDs, sanitized specifications, source-linked weights, price basis and capture time. The selected minimum entry is preserved separately even if it falls beyond the array limit. Representative-weight evidence includes source units, bounded raw samples, count and aggregation. Arrays are capped with truncation counts. It does not store full HTML, cookie jars, authentication headers, proxy settings or seller personal data, and it never adds a billable row.

### Supplier rollup: `SUPPLIER_ROLLUP` (free)

Your delivered rows grouped by `companyId` across the whole list. This is the "can I consolidate my catalog into fewer factories?" view. Suppliers covering the most of your products come first, then the cheapest. It needs no extra searches, costs nothing and is never written to the dataset, so it adds no billable rows. `SUPPLIER_ROLLUP.csv` holds the same data, with arrays joined by `;`.

| Field | Meaning |
| --- | --- |
| `companyId`, `companyName`, `yearsOnPlatform`, `verifiedFlag`, `province` | Supplier identity and signals |
| `matchedProductCount` | How many of your products have at least one shortlist row from this supplier |
| `matchedProductIndexes` | Those products' `inputIndex` values, to join back to your list |
| `offerCount` | Distinct offers from this supplier across your shortlists |
| `priceCnyMin`, `priceCnyMax` | Unit-price range across those rows |
| `landedCostUsdMin` | Lowest landed-cost estimate (from enriched rows) |
| `avgMatchScore` | Average `matchScore` of its rows |
| `bestRank` | Best `rank` it reached in any shortlist |
| `catalogCoverage` | How many of your products it appeared for in the candidate pools; the same count as `supplierCatalogCoverage`, and it can be higher than `matchedProductCount` |

The rollup covers one run. If you split a catalog across runs, merge the CSV files on `companyId`.

### Input status: `INPUT_STATUS` (free)

One entry per input product (`INPUT_STATUS.csv` for spreadsheets): `inputIndex`, `clientSku`, `inputAsin`, `inputType`, `status`, `rowsDelivered`, `enrichedRowsDelivered` and a `reason` when something failed. Title-only input entries are invalid and never searched; optional title labels on image entries are echoed in the Dataset.

| Status | Meaning | Charged |
| --- | --- | --- |
| `OK` | Delivered at least one row | Yes, see Pricing |
| `INVALID_PRODUCT` | No public photo URL, an unsafe/invalid image URL, or an unsupported title-only entry | No |
| `IMAGE_FETCH_FAILED` | The photo could not be downloaded or is not a usable image | No |
| `NO_MATCH` | 1688 returned no usable candidates, or none passed your filters | No |
| `RISK_CONTROL` | 1688's anti-bot layer refused the query | No |
| `NETWORK` | Network or proxy failure | No |
| `DEADLINE_SKIPPED` | Not reached within the run's time window | No |
| `SKIPPED_QUOTA` | Your `maxTotalChargeUsd` could not fully cover it | No |

The key-value store also holds `SUMMARY` (run report: charged vs intended events, counts, warnings) and `OUTPUT` (final `errorCode` and message).

### Pricing (pay per event)

| Event | FREE | BRONZE | SILVER | GOLD | PLATINUM | DIAMOND | Charged when |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `run-base-fee` | $0.01 | $0.01 | $0.01 | $0.01 | $0.01 | $0.01 | Once per run, only if the run delivers at least one row |
| `product-matched` | $0.010 | $0.009 | $0.008 | $0.007 | $0.007 | $0.007 | Per input product that delivers at least one row |
| `shortlist-row` | $0.003 | $0.0027 | $0.0024 | $0.0021 | $0.0021 | $0.0021 | Per delivered dataset row |
| `supplier-enriched` | $0.002 | $0.0018 | $0.0016 | $0.0014 | $0.0014 | $0.0014 | Per delivered row that received a full product-detail fetch (`enriched: true`) |

Apify applies the subscription tier of the account running the Actor. BRONZE saves 10%, SILVER 20%, and GOLD, PLATINUM and DIAMOND 30% on the three main events. The base fee is never discounted. `SUMMARY.eventPricesUsd`, the budget precheck and the reported charge amounts use this run's actual prices.

Nothing is charged for products that fail, don't match or are skipped. There is no start fee and no platform-usage pass-through: proxy and compute costs are included. The rollup, the status list and the summary are free.

**Worked examples at FREE-plan prices** (every product matched, every shortlist full):

| Order | Calculation | Total |
| --- | --- | --- |
| 2 photos × 5 offers, `enrichDepth: 1` (quick start) | $0.01 + 2 × $0.010 + 10 × $0.003 + 2 × $0.002 | **$0.064** |
| 10 products × 5 offers, `enrichDepth: 1` | $0.01 + 10 × $0.010 + 50 × $0.003 + 10 × $0.002 | **$0.28** |
| 10 products × 5 offers, `enrichDepth: 0` | $0.01 + 10 × $0.010 + 50 × $0.003 | **$0.26** |
| 30 products × 5 offers, `enrichDepth: 1` | $0.01 + 30 × $0.010 + 150 × $0.003 + 30 × $0.002 | **$0.82** |

At FREE-plan prices, the most a matched product can cost is $0.010 + `shortlistSize` × $0.003 + `enrichDepth` × $0.002, which is $0.027 with the defaults. The same default product costs at most $0.0243 on BRONZE, $0.0216 on SILVER and $0.0189 on GOLD or above, plus the once-per-run base fee. A product with a shorter shortlist (for example, fewer distinct suppliers) costs less.

**Budget cap.** `maxTotalChargeUsd` defaults to $20 (minimum $0.04). Before searching, the run uses your subscription-tier prices to set aside the base fee and each product's maximum cost. Products at the end of your list that the cap cannot fully cover are skipped up front (`SKIPPED_QUOTA`), never searched and never charged, and the run reports `QUOTA_BLOCKED`. Rows already delivered are always kept.

### Errors and run outcomes

| `errorCode` | What happened | Charge |
| --- | --- | --- |
| `NO_INPUT` | No products were sent, or every entry is empty | $0 |
| `NO_IMAGE_INPUT` | No product has a photo URL; title-only search is unsupported | $0 |
| `INPUT_TOO_LONG` | More products than one run accepts, or a CSV over 2 MB; split the list | $0 |
| `CSV_PARSE_FAILED` | The CSV is unreadable or has no image URL column; the message lists the columns it detected | $0 |
| `ENRICH_CAP_EXCEEDED` | Products × `enrichDepth` would exceed 400 full-detail fetches; the message suggests a valid configuration | $0 |
| `QUOTA_BLOCKED` | The budget cap could not cover every product; the rest are `SKIPPED_QUOTA` | Delivered rows only |
| `RISK_CONTROL_CANARY` | 1688's risk control blocked the opening image search on every attempt, so the photo products were not searched (`RISK_CONTROL`) | Delivered rows only |
| `CIRCUIT_BREAKER` | Image search stopped after repeated risk-control blocks; the remaining photo products are `RISK_CONTROL` | Delivered rows only |
| `PARTIAL_DEADLINE` | The time budget skipped some products (`DEADLINE_SKIPPED`) | Delivered rows only; skipped products are free |

The first five are validation errors: the run stops before any search. The `errorCode` is reported in `OUTPUT` and `SUMMARY`.

### What is not in the data

- **Reviews and ratings:** `reviewCount` and `rating` are not provided.
- **`responseRate`:** usually `null`.
- **Category path:** only `categoryId` is provided.
- **Personal data, excluded on purpose:** no seller login IDs, personal phone numbers, emails or messenger handles, no buyer reviews or Q\&A, and no person-level credit scores. Individual sellers have company IDs, company names, corporate badges and storefront URLs blanked. Keyword storefront labels are not published as registered company identities.

### Limits & caveats

- **Up to 100 products per run.** The default configuration (5 shortlist rows and 1 enrichment target per product) completed the private 100-product test within the 40-minute timeout. This is a workload limit, not a guarantee that every image matches or every field exists. Larger lists are rejected before any search (`INPUT_TOO_LONG`, no charge). More aggressive enrichment settings and shorter timeouts can cause partial delivery; use `INPUT_STATUS` and `SUMMARY` to check actual results.
- **Enrichment cap.** A run can fetch at most 400 full details (products × `enrichDepth`). This is separate from the per-run product limit.
- **Similar is not the same.** Visual similarity is not proof of the same product or of the original factory, and many 1688 sellers reuse the same photos. Title-only search is not supported, and `matchScore` only reflects 1688's result order. Confirm with the supplier and order samples.
- **Estimates are estimates.** `landedCostUSD` uses the MOQ-tier unit price, a fixed CNY/USD rate, a per-kg shipping estimate, a duty rate for your `targetMarket` and a generic fee allowance. It is not a quote for a larger bulk order at a deeper discount; check `priceLadder` for the full tier schedule. `landedCostBasis: "matched_sku"` means the price and weight are linked to one source SKU; any other basis is a rough minimum-price estimate with representative or inferred weight. It does not include Amazon FBA fees, storage, advertising or returns. `suggestedRetailUSD` and `estimatedMarginPct` come from a markup rule, not market prices. None of these figures is a quote or a profit forecast.
- **Prices are indicative unless `priceVerified` is `true`; even then, check the variant and date.** 1688 prices vary with quantity, variant and time. Use `priceLadder`, `priceBasis`, `priceEvidenceKey` and `scrapedAt`.
- **Badges come from 1688, not from us.** `verifiedFlag`, `isFactory` and `isSuperFactory` reflect what 1688 displays. `bizType` is a separate seller business-model label, so `Manufacturer` with `isFactory: false` can both come from 1688. We do not verify identity, licenses or production capacity.
- **Chinese-first source.** `titleCN` is authoritative, `titleEN` is a machine translation, and company names stay in Chinese.
- **1688 risk control.** Some queries are refused (`RISK_CONTROL`). Those products are not charged and are reported individually.
- **Compliance hints are keyword-based**: a starting point for your own checks, not legal advice.

### FAQ

#### Can I use this for Amazon FBA sourcing?

Yes. Send the products you plan to sell, using your own photo or a listing's main image, with your ASIN and SKU as labels. Each product gets a ranked 1688 shortlist, its top offer gets a landed-cost estimate for your `targetMarket` (US, EU, UK or AU) whenever the detail page confirms a price, and the rollup shows suppliers that could carry several of your products. Typical next steps: confirm the product and the supplier, request quotes, order samples and check compliance. The Actor does not check FBA eligibility, category restrictions or Amazon fees.

#### Does it work for product sourcing beyond Amazon?

Yes. Nothing is Amazon-specific apart from the labels you choose to send. Dropshipping and Shopify sellers get MOQ on every row and `dropshipScore` on enriched rows. Private label buyers can rank by `supplierYears` and filter on `isFactory` and `verifiedFlag`.

#### Can it look up my ASINs?

No. `asin` is only an echo label: it is copied unchanged to `inputAsin` so you can join results back to your sheet. Nothing is fetched from Amazon. To source an ASIN, send the listing's main image URL as `imageUrl`. A title may accompany the photo as a reference label but cannot replace it.

#### What CSV format does it accept?

- A header row is required. Use comma-separated UTF-8 text; a BOM, quoted fields (with commas, quotes or line breaks inside) and blank lines are all handled.
- Column names are case-insensitive. Photo: `image_url`, `imageUrl`, `image`, `img` or `image url`. Title: `title`, `name`, `product`, `product_name` or `product name`. ASIN: `asin`. SKU: `sku`, `clientSku`, `client_sku` or `id`. Other columns are ignored.
- You need a photo URL column. An optional title column is a reference label only. Missing photo columns return `CSV_PARSE_FAILED` (free), including the columns detected. Duplicate aliases, malformed quotes and unequal column counts are rejected rather than silently losing data.
- Copying cells straight out of Excel or Google Sheets produces tab-separated text. Export or download the sheet as CSV instead.
- `inputIndex` 0 is the first data row after the header.

#### Why do some rows have no landed cost?

By design. Only the top `enrichDepth` offers of each product (1 by default) get a full product-detail fetch. The other rows are list-level (`enriched: false`, `detailLevel: "list"`) and leave `landedCostUSD`, its basis and weight fields, `costSkuId`, `costSpec`, `priceEvidenceKey`, `suggestedRetailUSD`, `estimatedMarginPct`, `dropshipScore`, the compliance hints and `priceLadder` empty. Raise `enrichDepth` (up to `shortlistSize`, at $0.002 per enriched row) if you need these fields on more rows. Landed cost also needs a positive price read from the detail page, so an enriched row with `priceVerified: false` has none. If a detail fetch fails, the row stays list-level and the enrichment fee is not charged.

#### What does matchScore mean?

It is a relative score derived from 1688's result order. 1.0 is 1688's first result, and the score falls linearly with position (0.95, 0.90 and so on with the default `maxResultsPerProduct` of 20). 1688 returns no absolute similarity value, so `matchScore` is not a similarity percentage, it shifts when you change the pool size, and it should not be compared across products. Visual similarity is not proof of the same product or the original factory. `minMatchScore` (default 0) filters on this score; 0.5, for example, keeps roughly the top half of 1688's results.

#### Can I search by title alone?

No. The first release requires a public product photo URL for every product. Title-only searching did not pass the private validation gate and is not offered. You can include `title` alongside `imageUrl` as a reference label for joining results to your catalog.

#### How many products can I send in one run?

At most 100 products. The default 5-row shortlist and 1 enriched offer per product were tested at this size. Lists over 100 stop before searching with `INPUT_TOO_LONG` and cost nothing; split them across runs. Each run adds one $0.01 base fee only if it delivers rows. The first release does not support a 250-product run.

#### How do I find suppliers that cover many of my products?

Open `SUPPLIER_ROLLUP.csv`. Suppliers covering the most of your products are listed first, with their price range, lowest landed-cost estimate and `matchedProductIndexes`. To make every shortlist prefer those suppliers, set `sortBy: "supplierCoverage"`.

#### Do I need a 1688 account?

No account, login or cookies. Proxy and compute costs are included in the event prices.

#### Is this legal? What about personal data?

The Actor reads publicly visible 1688 listing data and deliberately leaves out personal data (see [What is not in the data](#what-is-not-in-the-data)). You are responsible for how you use the data and for complying with 1688's terms and applicable laws.

### API

Start a run:

```bash
curl -X POST "https://api.apify.com/v2/acts/crawleast~1688-supplier-finder/runs" \
  -H "Authorization: Bearer YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      { "imageUrl": "https://example.com/product-1.jpg", "clientSku": "SKU-001", "asin": "B0EXAMPLE1" },
      { "imageUrl": "https://cbu01.alicdn.com/img/ibank/15418323391_1847764777.jpg", "title": "Product reference", "clientSku": "SKU-002" }
    ],
    "shortlistSize": 5,
    "enrichDepth": 1,
    "sortBy": "landedCost",
    "targetMarket": "US",
    "maxTotalChargeUsd": 5
  }'
```

The response contains the run `id`. Once the run has finished, read the outputs:

```bash
RUN_ID="<run id>"
curl -H "Authorization: Bearer YOUR_APIFY_TOKEN" "https://api.apify.com/v2/actor-runs/$RUN_ID/dataset/items?format=csv"
curl -H "Authorization: Bearer YOUR_APIFY_TOKEN" "https://api.apify.com/v2/actor-runs/$RUN_ID/key-value-store/records/SUPPLIER_ROLLUP.csv"
curl -H "Authorization: Bearer YOUR_APIFY_TOKEN" "https://api.apify.com/v2/actor-runs/$RUN_ID/key-value-store/records/INPUT_STATUS"
```

Python (`pip install apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
with open("catalog.csv", encoding="utf-8") as f:
    run = client.actor("crawleast/1688-supplier-finder").call(run_input={
        "productsCsv": f.read(),
        "shortlistSize": 5,
        "enrichDepth": 1,
        "sortBy": "supplierCoverage",
    })

rows = client.dataset(run["defaultDatasetId"]).list_items().items
store = client.key_value_store(run["defaultKeyValueStoreId"])
rollup = store.get_record("SUPPLIER_ROLLUP")["value"]
status = store.get_record("INPUT_STATUS")["value"]
```

Long lists take minutes, so prefer starting the run and reading its storages afterwards over synchronous endpoints, which can time out.

### For AI agents and pipelines

Input, dataset, key-value store and output schemas are all declared, rows are flat (join on `inputIndex` or `clientSku`), `INPUT_STATUS` and `OUTPUT.errorCode` report per-product and run outcomes, and `maxTotalChargeUsd` caps spend. Copy this to your AI assistant:

```text
Use the Apify Actor crawleast/1688-supplier-finder to shortlist 1688 suppliers
for a product list. Batch products into as few runs as possible, sent as
"products" ([{"imageUrl": "...", "clientSku": "..."}]) or "productsCsv", with
shortlistSize 5, enrichDepth 1, sortBy "landedCost" and a maxTotalChargeUsd cap.
Read the dataset rows (one per product x offer), SUPPLIER_ROLLUP (suppliers that
cover several products) and INPUT_STATUS (what happened to each product).
Treat landedCostUSD and estimatedMarginPct as estimates, matchScore as a
relative position score, and visual matches as candidates to verify.
```

### Related Actors

- **[1688 Image Search Scraper](https://apify.com/crawleast/1688-image-search-scraper)**: up to 20 raw candidate cards per photo, base64 photo uploads and offer-ID detail lookups.
- **[1688 Product Scraper](https://apify.com/crawleast/1688-product-scraper)**: keyword research across 1688 categories, with English titles, MOQ price ladders and supplier signals.

### Feedback & changelog

Found a bug or missing a field? Open an **Issue** with the run ID. Release history is in the **Changelog** tab.

***

*Data is provided "as is" for research and sourcing decisions. Prices, landed-cost and margin figures, scores and compliance hints are estimates; verify independently before committing funds. Not affiliated with 1688.com, Alibaba Group or Amazon.*

# Changelog

This Actor's version history is a separate document: https://apify.com/crawleast/1688-supplier-finder/changelog.md

# Actor input Schema

## `products` (type: `array`):

Up to 100 products per run. Each product requires imageUrl: a public http(s) photo URL without credentials, up to 2048 characters. Optional title (up to 200 characters), asin (up to 32 characters) and clientSku (up to 64 characters) are echoed as labels for joining to your sheet. Title-only searching is not supported. Entries without a usable imageUrl are INVALID_PRODUCT and never charged. Lists over 100 entries return INPUT_TOO_LONG before searching and are never charged; split them across runs. The validated default is 5 offers per product with 1 enriched offer, not a guarantee that every image has a match.

## `productsCsv` (type: `string`):

Paste a raw CSV export (up to 2 MB and 100 data rows) instead of products. More rows are rejected before searching, never silently truncated. A header and an image URL column are required: image, image_url, imageUrl, img or image url. Optional label columns: title/name/product/product_name/product name, asin, and sku/clientSku/client_sku/id. Column names are case-insensitive. Missing image columns, duplicate aliases or malformed CSV return CSV_PARSE_FAILED with detected columns. Title-only search is unsupported. Ignored when products is filled in.

## `shortlistSize` (type: `integer`):

How many supplier offers to return per product (rows per product).

## `enrichDepth` (type: `integer`):

How many of the top shortlisted offers per product get a detail-page enrichment (price ladder, weight, estimated landed cost, margin, dropship score, compliance hints). Leave empty to follow "Enrich top supplier" (1 when on, 0 when off). 0 = list-level rows only. Values above Suppliers per product are reduced to it. Products × this value may not exceed 400 per run.

## `enrichDetails` (type: `boolean`):

Used only when Enriched suppliers per product is empty: on = enrich the top 1 offer per product, off = no enrichment.

## `maxResultsPerProduct` (type: `integer`):

How many 1688 search results per product are considered before sorting, de-duplication and the shortlist cut.

## `targetMarket` (type: `string`):

Destination market used for the estimated landed cost (duty rate) and compliance hints on enriched rows.

## `sortBy` (type: `string`):

Ranking of each product's shortlist. landedCost: enriched rows with an estimated landed cost first (cheapest first), then the rest by unit price; with no enrichment it falls back to unit price. Costs use the MOQ-applicable tier price, not the lowest bulk-quantity price; landedCostBasis and unitWeightBasis distinguish a source-linked SKU from a representative/inferred-weight rough estimate. price: lowest unit price first. supplierYears: longest on 1688 first. matchScore: 1688 result order. supplierCoverage: suppliers that also appear for more of your other products first, then price.

## `minMatchScore` (type: `number`):

Drop candidates below this matchScore (0–1). matchScore is a relative position score from the 1688 result order (1 = first result), not a visual-similarity percentage. 0 keeps every candidate.

## `supplierDedupe` (type: `boolean`):

Keep only the best-ranked offer of each identified company within a product's shortlist. Offers without reliable corporate identity cannot be deduplicated by supplier.

## `excludeSupplierIds` (type: `array`):

Supplier companyId values (from a previous run's companyId column) to leave out of every shortlist. Up to 1000.

## `maxTotalChargeUsd` (type: `number`):

Hard cap on pay-per-event charges for this run. Events: run-base-fee $0.01 once per run, only when at least one row is delivered; product-matched $0.010 per product that delivers at least one row; shortlist-row $0.003 per delivered row; supplier-enriched $0.002 per delivered enriched row. Products the cap cannot cover are skipped before searching (SKIPPED_QUOTA) and never charged; everything already delivered is kept. Failed, unmatched or skipped products are never charged.

## Actor input object example

```json
{
  "products": [
    {
      "imageUrl": "https://cbu01.alicdn.com/img/ibank/15418323391_1847764777.jpg",
      "asin": "B0EXAMPLE1",
      "clientSku": "SKU-001"
    },
    {
      "imageUrl": "https://cbu01.alicdn.com/img/ibank/O1CN01FCwJrl29btzNvQkHM_!!2235628087-0-cib.jpg_270x270xzq60.jpg",
      "title": "Product reference label",
      "clientSku": "SKU-002"
    }
  ],
  "shortlistSize": 5,
  "enrichDetails": true,
  "maxResultsPerProduct": 20,
  "targetMarket": "US",
  "sortBy": "landedCost",
  "minMatchScore": 0,
  "supplierDedupe": true,
  "excludeSupplierIds": [],
  "maxTotalChargeUsd": 20
}
```

# Actor output Schema

## `results` (type: `string`):

Flat rows, one per input product × 1688 offer: your echoed SKU/ASIN labels, matchScore, rank and rankBasis, offer, unit price and MOQ, supplier signals and supplierCatalogCoverage. Enriched rows (the top enrichDepth offers per product) also carry the price ladder, landed-cost, retail and margin estimates, a dropship score and compliance hints; rows that were not enriched leave these fields empty by design. Field meanings are described in the dataset schema.

## `supplierRollup` (type: `string`):

Delivered rows grouped by supplier across your whole list: matchedProductCount, matchedProductIndexes, offerCount, unit-price range, lowest landed-cost estimate, avgMatchScore, bestRank and catalogCoverage. Shows which suppliers could cover several of your products. Free; stored under the SUPPLIER_ROLLUP key.

## `supplierRollupCsv` (type: `string`):

The same supplier rollup as a CSV download for spreadsheets and BI tools; array values are joined with semicolons. Stored under the SUPPLIER_ROLLUP.csv key.

## `inputStatus` (type: `string`):

One entry per input product: its status (OK, INVALID_PRODUCT, IMAGE_FETCH_FAILED, NO_MATCH, RISK_CONTROL, NETWORK, DEADLINE_SKIPPED or SKIPPED_QUOTA) and the number of rows it delivered. Products without rows are not charged. Stored under the INPUT_STATUS key.

## `inputStatusCsv` (type: `string`):

The same per-product status list as a CSV download, ready to join back to your product sheet. Stored under the INPUT_STATUS.csv key.

## `runSummary` (type: `string`):

Machine-readable run report stored under the SUMMARY key: charged vs intended pay-per-event counts, charged USD, product and row counts, enrichment and time-budget diagnostics, warnings and the run errorCode.

## `runOutcome` (type: `string`):

Final outcome record stored under the OUTPUT key: errorCode (null on a clean run, or e.g. NO_INPUT, INPUT_TOO_LONG, CSV_PARSE_FAILED, ENRICH_CAP_EXCEEDED, QUOTA_BLOCKED, PARTIAL_DEADLINE) with a plain-English message, so integrations can detect no-op or partial runs without parsing the log.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "products": [
        {
            "imageUrl": "https://cbu01.alicdn.com/img/ibank/15418323391_1847764777.jpg",
            "clientSku": "SKU-001"
        },
        {
            "imageUrl": "https://cbu01.alicdn.com/img/ibank/O1CN01FCwJrl29btzNvQkHM_!!2235628087-0-cib.jpg_270x270xzq60.jpg",
            "clientSku": "SKU-002"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawleast/1688-supplier-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "products": [
        {
            "imageUrl": "https://cbu01.alicdn.com/img/ibank/15418323391_1847764777.jpg",
            "clientSku": "SKU-001",
        },
        {
            "imageUrl": "https://cbu01.alicdn.com/img/ibank/O1CN01FCwJrl29btzNvQkHM_!!2235628087-0-cib.jpg_270x270xzq60.jpg",
            "clientSku": "SKU-002",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("crawleast/1688-supplier-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "products": [
    {
      "imageUrl": "https://cbu01.alicdn.com/img/ibank/15418323391_1847764777.jpg",
      "clientSku": "SKU-001"
    },
    {
      "imageUrl": "https://cbu01.alicdn.com/img/ibank/O1CN01FCwJrl29btzNvQkHM_!!2235628087-0-cib.jpg_270x270xzq60.jpg",
      "clientSku": "SKU-002"
    }
  ]
}' |
apify call crawleast/1688-supplier-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawleast/1688-supplier-finder"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9BePu9Fs6kr96U4ic/builds/apO1D04tQdgtXtT1c/openapi.json
