# Price List to CSV – Clean Price Sheet Parser (`jc-datatools/price-list-normalizer`) Actor

Turn a messy price list, rate card, supplier sheet or service menu into a clean CSV, Excel or JSON table in 3 steps: paste it or link it, choose CSV, click Start. One row per item with price, unit and currency. Rule-based, so it never invents a price. $0.20 per list.

- **URL**: https://apify.com/jc-datatools/price-list-normalizer.md
- **Developed by:** [Joao Cassimiro](https://apify.com/jc-datatools) (community)
- **Categories:** Developer tools, Automation, Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $140.00 / 1,000 price list processeds

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

**Turn a messy price list, rate card or service menu into a clean CSV or JSON table.** One row per item with the **price**, **unit**, **currency** and a **flag** on anything worth a second look. Paste the text, or give a public web page, `.txt`, `.csv` or text PDF link, then press **Start**.

▶️ **Watch the 39-second demo:** messy price sheet to clean CSV in 3 steps.

https://www.youtube.com/watch?v=Cdagl72L2uI

### How it works (3 steps)

1. **Paste** your price list into **Price list (paste it here)**. Or put a link to a public web page, `.txt`, `.csv` or text PDF file in **Price list URL**.
2. **Choose CSV** under **Output format** (or keep JSON).
3. **Click Start.** When the run finishes, the **Output** tab shows one clean row per item. Download it as CSV, Excel or JSON.

Works for a **supplier price list**, **rate card**, **service menu**, **restaurant or catering menu**, **contractor price sheet** or **price book**. Have a **PDF price list**? Put its link in **Price list URL** (see the FAQ).

By default it works by rules, not AI, so **it never invents a price**: every number comes from your sheet, next to the original text (`price_text`, `raw`) and line number (`source_line`).

| You give it | You get back | You pay |
|---|---|---|
| Pasted text, or a public web page, `.txt`, `.csv` or text PDF URL | One row per item: description, price or low–high range, unit, currency, flags, source line. Plus a SUMMARY of every line that wasn't read as an item | **$0.20 per price list** (up to 1,000 rows). **$0** if no prices are found |

### What does this price list parser do?

It reads each line of a price sheet and decides what the item is, what it costs and in what unit. You get:

- **description**: the item or service, with the price cut out. Minimums, add-ons and notes stay in the text.
- **price**: a single number. Or **price_low / price_high** for a range such as `$40-$60/hr`. Or **non_price** with the sheet's own words, such as "call for pricing", "TBD", "price on request" or "negotiable".
- **unit**, normalized: each, hour, day, week, month, year, visit, flat, sq ft, 1k sq ft, lin ft, cu yd, cu ft, yard, load, trip, mile, gal, lb, ton, pane, vent, site, item, application.
- **multi-buy deals**: for "2 for $5", `price` is the per-unit price (2.50, unit `each`). `deal_quantity` (2) and `deal_total` (5) keep the deal as written.
- **currency**: USD, EUR, GBP or JPY from the symbol ($ is read as USD), or a written code (USD, EUR, GBP, CAD, AUD, NZD, JPY). Left empty when the sheet doesn't say.
- **quantity**: counts such as "3 units" or "2 @ $5", kept separate so a count is never taken as a price.
- **tiers**: when a row has more than one per-unit price (side-by-side columns such as residential and commercial, or a discounted price), all of them are kept under the keys `primary`, `alt_1`, `alt_2`, and `deal_N_for_M` for multi-buy totals.
- **flags** on uncertain rows, plus **source_line** and **raw** on every row.

It reads dot leaders (`Mow ....... $45`), dashes, tabs, bullets and real CSV files. For a CSV with headers, it finds the item, price, unit and quantity columns by their names. For a web page, it pulls out HTML table rows and the remaining text.

### Why use it instead of asking a chatbot? It never invents a price

A chatbot that can't read a line still returns a plausible number, so a minimum charge or trip fee becomes the price and nobody notices. This Actor works the other way round:

- **No made-up prices.** If a line has no price, you get the sheet's own words ("call for pricing"), never a number.
- **The right number on the line.** Minimums, add-ons and surcharges aren't taken as the price. "Hedge trimming - $3.50 per linear ft, minimum $85" comes out as 3.50 per lin ft, with "minimum $85" kept in the description.
- **Every row can be traced.** `price_text` is the price exactly as written, `raw` is the whole original line, and `source_line` is its line number, so you can check any row against the sheet in seconds.
- **Nothing disappears.** Headings and terms lines (like "All prices subject to a 3% fuel surcharge") are listed in the **SUMMARY** record with a reason, not dropped silently or turned into fake items. A line with a number but no readable price stays as a row flagged `no_price_found`.
- **Same input, same output**, which matters in pipelines and agent workflows.

These promises apply to the default rule-based mode. Optional AI mode (below) uses a language model and is less predictable.

#### What do the flags mean?

There are 14 flag types. A row can have several.

| Flag | What it means |
|---|---|
| `duplicate_conflicting_price` | The same item appears more than once with different prices |
| `price_outlier_high` | At least 20× the median price for its unit (checked when 5+ rows share that unit); often a total row or typo |
| `price_outlier_low` | 1% of the median for its unit or less (same 5-row rule) |
| `contradictory_pricing` | A price **and** a phrase like "call for quote" on the same line |
| `approximate_price` | "from" or "starting at" |
| `ambiguous_number_format` | `1.250` could be 1.25 or 1,250 (read as 1.25) |
| `multiple_price_candidates` | Several figures on one line; the likeliest is the price, all are in `tiers` |
| `range_present` | A range sits beside a single price ("Crew of 2-3 movers $120/hr") |
| `range_reversed` | A range written high to low; stored low to high |
| `multibuy_deal` | A deal like "2 for $5"; per-unit price worked out, deal kept |
| `unit_price_rounded` | The per-unit split isn't exact ("3 for $10" = 3.33) |
| `ambiguous_at_price` | "2 @ $5" read as 2 at $5 each |
| `promotion` | "2 for 1", BOGO or similar |
| `no_price_found` | Looks like an item, but no price could be read (not billed) |

### Who uses a price list to CSV converter?

- **Contractors and home-service businesses**: landscaping, cleaning, HVAC or plumbing price sheets going into Jobber, Housecall Pro, QuickBooks or a spreadsheet.
- **Restaurants and caterers**: menus and catering price lists going into a POS or website.
- **Procurement and ops teams**: supplier and vendor rate cards put into one format so they can be compared.
- **Agencies**: price pages and quote calculators for local clients.
- **Developers and AI-agent builders**: a price-list parser that gives the same answer every time.

### How do I convert a price list to CSV or Excel?

1. Click **Try for free**.
2. Paste your price list into **Price list (paste it here)**, or enter a **Price list URL**.
3. Under **Output format**, choose **JSON** or **CSV**, then click **Start**.
4. Open the **Output** tab to see the table, and export it as CSV, JSON or Excel. If you chose CSV, a ready-to-open `OUTPUT.csv` is also saved in the run's key-value store.

**Options:**

- **Reject rows with no price** (off by default) keeps only rows with a number. "Call for pricing" rows are dropped.
- **Use an LLM for genuinely unstructured input** (off by default) is described under AI mode below.

#### Input example

This is the example pre-filled in the input form:

```json
{
  "priceListText": "Lawn & Landscape\nMow, edge, blow .......... $45 / visit\nMowing larger lot (1/2 acre) ..... $65\nHedge trimming - $3.50 per linear ft, minimum $85\nLeaf cleanup - $150 flat, add $50 if more than 6 bags\nMulch install - $85 per cubic yard, includes labor\nWeed/feed 7-step program: call for pricing\nSpring cleanup bundle (leaf + bed + first mow) $299\nFertilizer application $60/1k sq ft\nAeration & overseed $0.18 per sq ft\nSnow removal (seasonal contract) - quoted per site, typical $95/visit\n\nALL PRICES SUBJECT TO A 3% FUEL SURCHARGE. Free estimates. Call Mike at (515) 555-0142.",
  "outputFormat": "csv"
}
```

#### Output example

This is real output from the input above (first rows shown; one dataset item per row; fields that don't apply are left out):

```json
[
  {"description": "Mow, edge, blow", "price": 45.0, "price_text": "$45 / visit", "currency": "USD", "unit": "visit", "source_line": 2, "raw": "Mow, edge, blow .......... $45 / visit"},
  {"description": "Hedge trimming - minimum $85", "price": 3.5, "price_text": "$3.50 per linear ft", "currency": "USD", "unit": "lin ft", "source_line": 4, "raw": "Hedge trimming - $3.50 per linear ft, minimum $85"},
  {"description": "Leaf cleanup - flat, add $50 if more than 6 bags", "price": 150.0, "price_text": "$150", "currency": "USD", "unit": "flat", "source_line": 5, "raw": "Leaf cleanup - $150 flat, add $50 if more than 6 bags"},
  {"description": "Weed/feed 7-step program", "non_price": "call for pricing", "source_line": 7, "raw": "Weed/feed 7-step program: call for pricing"},
  {"description": "Aeration & overseed", "price": 0.18, "price_text": "$0.18 per sq ft", "currency": "USD", "unit": "sq ft", "source_line": 10, "raw": "Aeration & overseed $0.18 per sq ft"}
]
```

The same rows in `OUTPUT.csv` use these columns:

```
description,price,price_low,price_high,unit,currency,quantity,deal_quantity,deal_total,non_price,tiers,flags,source_line
"Mow, edge, blow",45.0,,,visit,USD,,,,,,,2
Hedge trimming - minimum $85,3.5,,,lin ft,USD,,,,,,,4
Weed/feed 7-step program,,,,,,,,,call for pricing,,,7
```

In the CSV, flags are joined with `;` and `tiers` is JSON.

The **SUMMARY** record (key-value store) gives counts, warnings and every line that wasn't read as an item:

```json
{
  "items": 10, "priced": 9, "unpriced": 1, "unparsedLines": 2, "flagged": 0,
  "flagCounts": {}, "warnings": [],
  "unparsed": [
    {"line": 1, "text": "Lawn & Landscape", "reason": "no price and no number - probably a heading"},
    {"line": 13, "text": "ALL PRICES SUBJECT TO A 3% FUEL SURCHARGE. Free estimates. Call Mike at (515) 555-0142.", "reason": "terms or note, not a line item"}
  ]
}
```

`warnings` also catches problems across rows: duplicates with different prices, outliers, and sheets where most priced rows have no unit.

### Try it with one of these

Four ready-made inputs. Paste any of the first three into the text box, or point the URL
field at the fourth, and you get the table shown without formatting anything yourself.

#### 1. A supplier price list — the case that breaks naive parsers

Every size on this sheet is a number, and a parser that takes "the number on the line"
prices the water bottle at **750**.

```
Valley Wholesale - Beverage Price List (effective 1 Nov 2026)

Still water, 750ml bottle          VW-750     $2.40 ea
Sparkling water, 1L bottle         SP-1L      $3.15
Orange juice, 2L                   OJ-2L      $4.50 / bottle
Cold brew concentrate, 1L          CB-1L      $12.00
Mineral water, 12 x 500ml case     MW-12      $18.00 / case
Tonic syrup, 500ml                 TS-500     call for pricing
```

**6 rows**, five with prices — 2.40, 3.15, 4.50, 12.00, 18.00 — and units of `each`,
`bottle` and `case`. `Tonic syrup` returns with `call for pricing` and no price. The dated
title produces no row at all.

#### 2. A service rate card — per-unit rates and stated minimums

```
GreenLeaf Lawn & Landscape - Rate Card

Mow, edge, blow                    $45 / visit
Hedge trimming                     $3.50 per linear ft, minimum $85
Weed / feed 7-step program         call for pricing
Aeration & overseed                $0.18 per sq ft
Mulch install                      $85 per cubic yard, includes labour
Snow removal (seasonal)            quoted per site, typical $95 / visit
Spring cleanup bundle              $299
```

**7 rows**, with units of `visit`, `lin ft`, `sq ft` and `cu yd`. `Hedge trimming` keeps
`minimum $85` attached to the 3.50 rate it constrains, rather than reporting 85 as the
price or discarding the conditional.

#### 3. A messy quote sheet — ranges, TBD and "from"

```
Tree Services Quote Sheet - updated 27 Oct

Tree removal (up to 24 in dia.)     $450 - $1,200
Stump grinding                      $8 per inch
Emergency call-out (after hours)    $250
Crown thinning (over 60 ft)         TBD
Lot clearing                        from $1,800
    (All prices subject to a 3% fuel surcharge. Free estimates.)
```

**5 rows.** The range stays a range (`450`–`1200`) instead of becoming two guesses; `TBD`
survives as a row because its absence is information; `from $1,800` is reported and
flagged `approximate_price` rather than presented as a firm price; and the surcharge
footnote is not a row.

#### 4. A PDF — the same job from a document

| | |
|---|---|
| Sample PDF | https://jmgc-drop.pages.dev/actor-samples/sample-price-list.pdf |
| Size | one page, 2.5 KB, text-based (no OCR needed) |
| Contents | fictional business data — "Meadowbrook Supply Co" |

Put that URL in the **Price list URL** field. **14 rows** come back, covering a dated
title, a percentage footnote, size numbers inside descriptions, a `call for pricing` row,
a range, a `from` price and a `TBD` — and the units `each`, `bottle`, `case`, `visit`,
`lin ft`, `sq ft`, `cu yd` and `in`.

### How much does it cost to convert a price list?

You pay per event. There's no subscription and no monthly fee for this Actor.

| What you run | Cost |
|---|---|
| 1 price list (up to 1,000 rows) | $0.20 (less on paid Apify plans, see below) |
| 10 price lists | $2.00 |
| A 1,500-row catalog | $0.40 (one charge per started 1,000 rows) |
| Optional AI mode, only when it's on and returns rows | +$0.03 per list (paid Apify plans only; free-plan users aren't charged it) |
| Empty input, or nothing with a price, range or "call for pricing" found | $0 |

- Only rows with a price, range or "call for pricing"-type phrase count toward the 1,000. Text with bare numbers can still be read as a price list and charged, so check the input first.
- **Paid Apify plans pay less per list:** $0.18 on Bronze (Starter), $0.16 on Silver (Scale), $0.14 on Gold (Business).
- **Free plan:** Apify's free plan includes $5 of usage each month, enough for up to 25 price lists at $0.20, so you can test the Actor on your own sheets first.

### How do I import a price list into Jobber or Housecall Pro?

Run with **CSV** output, open `OUTPUT.csv` in a spreadsheet, check flagged rows, then rename and add columns as below. Column names are from each tool's official help page (checked Oct 28, 2026). The Actor doesn't write these files directly, and import formats can change, so check the current instructions first.

#### Jobber (Settings → Products & Services → Import Products & Services)

Jobber's [help page](https://help.getjobber.com/en/articles/products-services-list/) says to download its sample CSV and keep these headings: **Name, Description, Unit Price, Taxable, Category, Active**.

| Jobber heading | Take it from | Notes |
|---|---|---|
| Name | `description` | |
| Description | `unit` (e.g. "per visit"), plus any notes | Jobber's sample file has no unit column |
| Unit Price | `price` | Numbers only; Jobber says a `$` breaks the import. The Actor's `price` column is already a plain number |
| Taxable | You fill in TRUE or FALSE | Blank means taxable |
| Category | You fill in `Service` or `Product` | Case-sensitive; blank means Product |
| Active | You fill in TRUE or FALSE | |

- **Rows without a single price:** ranges (`price_low` / `price_high`) and `non_price` rows have no single number, so decide those by hand.
- **Large lists:** Jobber limits one import to 2.5 MB.
- **QuickBooks sync:** if you sync with QuickBooks US, Jobber says to update items in QuickBooks instead.

#### Housecall Pro (Settings → Price Book → Services or Materials → Upload file)

Housecall Pro's [help page](https://help.housecallpro.com/en/articles/7173129-price-book-import-or-export-services-and-materials) lists these service fields: **Industry** (required, must match an industry in your account), **Category** (required), **Subcategory\_1**, **Name** (required), **Description**, **Price**, **Cost**, **Taxable** and **unit_of_measure** (defaults to "each").

| Housecall Pro field | Take it from |
|---|---|
| Name | `description` |
| Price | `price` |
| unit_of_measure | `unit` |
| Industry, Category | You fill these in |

Its import wizard matches your columns to its fields and highlights errors before upload. Its examples spell units out ("square foot", "gallon"), while the Actor writes "sq ft" and "gal", so check how the wizard accepts them.

### Can I use it with Zapier, Make, n8n or the API?

Yes. Apify's official integrations can start any Store Actor:

- **Zapier:** the Apify app's **Run Actor** action. Synchronous runs must finish within 30 seconds; a typical price list finishes well inside that.
- **Make:** **Run an Actor**, with **Watch Actor Runs** to pick up results.
- **n8n:** the Apify node's **Run an Actor and get dataset**.
- **Sheets, Airtable or a CRM:** send the dataset on from any of these, use a webhook when the run finishes, or schedule runs against a public price page.

From code, call `jc-datatools/price-list-normalizer`. The **API** tab has ready-made JavaScript, Python, CLI and HTTP examples. One HTTP call that runs it and returns the rows:

```bash
curl -X POST "https://api.apify.com/v2/acts/jc-datatools~price-list-normalizer/run-sync-get-dataset-items" \
  -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"priceListText": "Mow, edge, blow .... $45 / visit\nHedge trimming - $3.50 per linear ft, minimum $85"}'
```

### Can AI agents use it? (MCP)

Yes. AI assistants and agents that support the Model Context Protocol, such as Claude, Cursor or VS Code, can call this Actor as a tool through Apify's hosted MCP server. Add this to your MCP client's configuration:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=jc-datatools/price-list-normalizer"
    }
  }
}
```

On first connection your browser opens so you can sign in to Apify. You can use an `Authorization: Bearer <YOUR_APIFY_TOKEN>` header instead. Loading the Actor as a tool also gives the agent the tools it needs to read the run's results (dataset items and the SUMMARY record). Agents connected to Apify's default MCP setup can also find it with `search-actors`.

**Why it suits agents:**

- Same answer for the same sheet, in the default mode.
- Every price traces to `source_line`, and unpriced items come back as words, not guesses.
- Agents can cap spending per run with `maxTotalChargeUsd` in the run options.

### What won't it do? (limits)

- **PDFs: text-based only, by link.** A PDF link in **Price list URL** is read when the PDF contains real, selectable text. A PDF can't go in the paste box; use its link. PDF links served as `application/octet-stream` work too.
- **PDFs that are refused, with a message saying why:** scanned or image-only PDFs (there's no OCR, so paste the text instead), password-protected PDFs (ones that open without a password and only block printing or copying are fine), PDFs over 40 pages, PDFs that take more than 15 seconds to read or hold far more content than a price list, and corrupt or truncated files. A refused PDF is not charged.
- **No images, Word or Excel files.** Paste the text, or link a web page, `.txt`, `.csv` or text PDF file. Links to image, ZIP, Word or Excel files are refused with a message.
- **Not handled yet:**
  - Prices written in words ("forty dollars").
  - "FREE": the line is listed as unparsed.
  - Bundles with a unit word in between: "2 cords for $500" is read as $500 with a `multiple_price_candidates` flag, not $250 per cord.
  - Units outside the list above (for example "acre") are left empty.
- **No price conversion.** It doesn't convert currencies or add taxes or surcharges. It reports what the sheet says.
- **URLs:** public http/https only (ports 80 and 443), at most 3 redirects, 5 MB maximum. Private and internal addresses are refused.
- **Review flagged rows before you import.** On a line with several figures, the most likely one is chosen and flagged.

### Optional AI mode

For price lists written as sentences instead of a list, turn on **Use an LLM for genuinely unstructured input**.

- Off by default. Choose Gemini 2.5 Flash (recommended) or Gemini 2.0 Flash. The first 20,000 characters are sent through Apify's OpenRouter integration.
- If the model fails or returns nothing usable, the parser output is kept and no AI fee is charged.
- AI-mode rows don't carry `source_line`, `raw` or `price_text`, and results can vary between runs. The cross-row checks still run. Use the default mode whenever your sheet is a list.

### FAQ

#### Can I convert a PDF price list to a spreadsheet (CSV or Excel)?

Yes, if the PDF has real text in it. Put the PDF's public link in **Price list URL**, choose CSV, and press **Start**. From the Output tab you can download CSV or Excel. A PDF on your computer needs a public link first, or copy its text out and paste it in. Scanned PDFs have no text to read, so they need OCR first.

#### Can I parse a supplier price list, rate card or menu?

Yes. Any list where each line has an item and a price works: supplier and vendor price lists, rate cards, service menus, restaurant and catering menus, and contractor price sheets.

#### Does it use AI or make up prices?

Not by default. The default mode is rule-based and never invents a price; a line with no price comes back with the sheet's own words. AI mode is off unless you turn it on.

#### What happens to minimum charges, trip fees and add-ons?

They're never taken as the price. They stay in the description ("Hedge trimming - minimum $85", price 3.50 per lin ft).

#### Can I import the result into Jobber, Housecall Pro or QuickBooks?

Yes, after renaming a few columns (see the Jobber and Housecall Pro section). For QuickBooks, use its own import template and map the same columns.

#### Which currencies and units does it understand?

$, €, £ and ¥ symbols, and the written codes USD, EUR, GBP, CAD, AUD, NZD and JPY. It reads 25 units, from each, hour and visit to sq ft, lin ft, cu yd, gal and ton. It reads European decimal commas ("25,50 €" = 25.50) and flags numbers that could go either way.

#### What if my price list has thousands of items?

It works. You pay $0.20 per started 1,000 priced rows, so 1,500 rows cost $0.40.

#### Am I charged if nothing is found?

No. If no price, range or "call for pricing" item is found, or the input is empty, nothing is charged.

#### Is my price list stored or shared?

Input and results stay in your own Apify storage. Only with AI mode on is the text sent to the selected model (through Apify's OpenRouter integration).

#### A line came out wrong. What should I do?

Open an issue on the **Issues** tab and paste the line that came out wrong and what you expected. Fixes for real-world lines are the priority.

### Feedback: tell me what you like, what you don't, and what broke

This Actor is maintained by Joao Cassimiro (jc-datatools). Your feedback decides what gets fixed and built next.

- 🐞 **Something came out wrong?** Open the **Issues** tab on this page. Paste the line, what you got and what you expected. Please remove anything private first.
- 💡 **Want a new format, unit or industry?** Open an issue that starts with "Request:".
- 👍 **Did it save you time?** A rating or a short review on this page helps other people find it.
- 👎 **Didn't fit your job?** Open an issue and tell me why, even in one line.

Every issue is read.

### Changelog

- **2026-10-29 (0.2.4)**: Coupon and discount wording (for example "Get $15 off hand wash" or "Coupon: $20 off season passes") no longer reports the discount as the item price, and lines with both a saving and a real price now use the real price. No output format changes.
- **2026-10-29 (0.2.3)**: Better handling of "off" wording (for example "off road tires" and "$X off <noun>" promos). No output format changes.
- **2026-10-29 (0.2)**: More accurate prices. A SKU or part code (`A-100`) is no longer read as the price; "save $20", discounts, deposits and refundable amounts are no longer taken as the price; "$99 / 2 hrs" keeps its price; tiers hold per-unit prices only. **Breaking:** `tiers` keys changed from `column_2`, `column_3`, ... to `primary`, `alt_1`, `alt_2`, ... and `deal_N_for_M`.
- **2026-10-28**: First public release.

# Changelog

This Actor's version history is a separate document: https://apify.com/jc-datatools/price-list-normalizer/changelog.md

# Actor input Schema

## `priceListText` (type: `string`):

Paste the text of your price list, rate card or service menu. Nothing needs to be formatted - dot leaders, dashes, tabs and columns are all handled. Use this or the URL below, not both. Three ready-made examples are in the README on this page - a supplier price list, a service rate card and a messy quote sheet - and a fourth arrives as a PDF through the URL field below.

## `priceListUrl` (type: `string`):

Fetch the price list from a public http or https URL instead of pasting it. Plain text, CSV, HTML or text PDF - table rows are extracted either way. Private and internal addresses are refused.

## `outputFormat` (type: `string`):

JSON is written to the dataset. Choosing CSV also writes a ready-to-open file to the key-value store as OUTPUT.csv.

## `useLlm` (type: `boolean`):

Off by default, and the parser handles most real price lists without it. Turn this on when the sheet is written as prose or sentences rather than a list, because the parser will then find fewer items than exist. AI mode adds a separate $0.03 charge per price list, only when it is on and returns rows. Text beyond 20,000 characters is not sent to the model.

## `llmModel` (type: `string`):

Both options are fast, low-cost models that work well for extracting price lists.

## `strictValidation` (type: `boolean`):

Off by default so that 'call for pricing' and 'TBD' survive as rows - they are real answers and their absence is information. Turn on to keep only rows that carry an actual number.

## Actor input object example

```json
{
  "priceListText": "Still water, 750ml bottle          VW-750     $2.40 ea\nHedge trimming                     $3.50 per linear ft, minimum $85\nWeed / feed 7-step program         call for pricing",
  "priceListUrl": "https://jmgc-drop.pages.dev/actor-samples/sample-price-list.pdf",
  "outputFormat": "json",
  "useLlm": false,
  "llmModel": "google/gemini-2.5-flash",
  "strictValidation": false
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

## `csv` (type: `string`):

No description

# 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 = {
    "priceListText": `Valley Wholesale - Beverage Price List (effective 1 Nov 2026)

Still water, 750ml bottle          VW-750     $2.40 ea
Sparkling water, 1L bottle         SP-1L      $3.15
Orange juice, 2L                   OJ-2L      $4.50 / bottle
Cold brew concentrate, 1L          CB-1L      $12.00
Mineral water, 12 x 500ml case     MW-12      $18.00 / case
Tonic syrup, 500ml                 TS-500     call for pricing`
};

// Run the Actor and wait for it to finish
const run = await client.actor("jc-datatools/price-list-normalizer").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 = { "priceListText": """Valley Wholesale - Beverage Price List (effective 1 Nov 2026)

Still water, 750ml bottle          VW-750     $2.40 ea
Sparkling water, 1L bottle         SP-1L      $3.15
Orange juice, 2L                   OJ-2L      $4.50 / bottle
Cold brew concentrate, 1L          CB-1L      $12.00
Mineral water, 12 x 500ml case     MW-12      $18.00 / case
Tonic syrup, 500ml                 TS-500     call for pricing""" }

# Run the Actor and wait for it to finish
run = client.actor("jc-datatools/price-list-normalizer").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 '{
  "priceListText": "Valley Wholesale - Beverage Price List (effective 1 Nov 2026)\\n\\nStill water, 750ml bottle          VW-750     $2.40 ea\\nSparkling water, 1L bottle         SP-1L      $3.15\\nOrange juice, 2L                   OJ-2L      $4.50 / bottle\\nCold brew concentrate, 1L          CB-1L      $12.00\\nMineral water, 12 x 500ml case     MW-12      $18.00 / case\\nTonic syrup, 500ml                 TS-500     call for pricing"
}' |
apify call jc-datatools/price-list-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jc-datatools/price-list-normalizer"
        }
    }
}
```

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/EzcByob6YeGsCtPUI/builds/GLbPHUfgsQY7Wsfcd/openapi.json
