Conversion
XML to CSV product feed
A supplier sends XML, your store or channel wants CSV. The conversion is a flattening (one product or variant becomes one row) plus a mapping (each field becomes a column with the name the destination expects). How much work that is depends on the XML. A Google Shopping–shaped feed (RSS or Atom, one flat item per product) is already a table in disguise, and Textfinity reads it directly by URL. A supplier's own schema with nested images, variants and attributes has to be flattened first; the short script below does that, and Textfinity takes the resulting CSV from there on a schedule.
We connect your store, build the feed and keep it in sync. Updated 2026-10-27.
At a glance
- Textfinity reads XML in Google Shopping's shape only: RSS 2.0 (<rss><channel><item>) or Atom (<feed><entry>), one product per item, fields as flat child elements; the g: prefix is optional.
- Nested elements inside an item (<images><image>, <variants><variant>) are skipped and attributes such as currency="EUR" are not columns, so a custom supplier schema is flattened to CSV first, one row per variant.
- The flattened CSV (or the Google-shaped XML) is read by HTTPS or SFTP URL on the shop's sync schedule; Textfinity maps it to the destination's column names and publishes a CSV or TSV on a stable URL.
- TSV to CSV is the same job with a delimiter change; Textfinity detects comma, tab, semicolon and pipe on import.
Which XML Textfinity reads, and what has to be flattened
Start by opening the file and looking at the first product. The question is not whether it is XML but whether each product is one flat element with one value per child. If it is, it converts as-is. If products nest lists or variants, or carry data in attributes, those parts need a flattening step before any feed tool that expects one row per item can use them.
| What the XML contains | Read directly? | What to do |
|---|---|---|
| <rss><channel><item> with <g:id>, <g:price> … | Yes | Each g: child becomes a column; most WooCommerce, Magento and PrestaShop Google feed plugins emit this |
| <rss><channel><item> with plain children (<code>, <qty>) | Yes | The g: prefix is optional; the children become columns under their own names |
| <feed><entry> (Atom) | Yes | Same as RSS: one entry per product, flat children |
| <products><product> or another custom root | No | Flatten to CSV with a script, or ask the supplier or plugin for a Google-format feed |
| <images><image> repeated inside an item | Skipped | Flatten: first URL to image_link, the rest joined with commas into additional_image_link |
| <variants><variant> inside a product | Skipped | Flatten: one row per variant, the parent SKU repeated as item_group_id |
| <price currency="EUR">189.00</price> | Value only | The attribute is not a column; write "189.00 EUR" during flattening or add suffix " EUR" in mapping |
| <description><![CDATA[<p>…</p>]]></description> | Yes | Read as text including the HTML; remove from regexp <[^>]+> in mapping |
| A Magento or PrestaShop custom XML export | No | Use the platform's Google feed module or its CSV export instead |
Element names are illustrative; the mapping screen shows the columns Textfinity detects in your file. Google's RSS 2.0 structure from its product data source help, checked 2026-10-27.
What custom supplier XML looks like and why it needs flattening
Three structures cause most of the trouble. Repeated elements (several <image> children) have to become either one column with a delimiter or several numbered columns. Attributes on elements (<price currency="EUR">) carry data that a naive converter drops. And nested variants (<variants><variant>) mean one product element maps to several CSV rows, not one. A generic online XML-to-CSV converter flattens the first level and silently loses the rest; Textfinity skips nested elements by design, which is why the flattening happens before it, in a step you control and can check.
supplier.xml (one product, two variants)
<products>
<product>
<sku>LAMP-ARC</sku>
<name>Arc Floor Lamp</name>
<brand>Lumen</brand>
<description><![CDATA[<p>Brushed steel arc lamp, 210 cm.</p>]]></description>
<price currency="EUR">189.00</price>
<url>https://supplier.example/p/lamp-arc</url>
<images>
<image>https://cdn.supplier.example/lamp-arc-a.jpg</image>
<image>https://cdn.supplier.example/lamp-arc-b.jpg</image>
</images>
<category><path>Lighting > Floor lamps</path></category>
<variants>
<variant><sku>LAMP-ARC-STEEL</sku><finish>Steel</finish><stock>7</stock></variant>
<variant><sku>LAMP-ARC-BRASS</sku><finish>Brass</finish><stock>0</stock></variant>
</variants>
</product>
</products>Flatten it to one row per variant
If the supplier cannot send a Google-format feed, a short script turns the custom XML into a CSV with one row per variant. The Python below uses only the standard library: it downloads the XML, writes the product fields once per variant, splits the image list, keeps the currency attribute next to the price, and lets the csv module quote commas and HTML safely. Run it on cron before the shop's sync and write supplier.csv to a web server or an SFTP server; Textfinity pulls from either. A product without a variants block becomes one row with its own SKU as both id and item_group_id.
xmlstarlet can do a flat schema in one command, but it does not quote fields that contain commas, which every HTML description does. Use a real CSV writer for anything a channel will read.
flatten_supplier.py
# usage: python3 flatten_supplier.py https://supplier.example/feed.xml
import csv
import sys
import urllib.request
import xml.etree.ElementTree as ET
root = ET.parse(urllib.request.urlopen(sys.argv[1])).getroot()
with open("supplier.csv", "w", newline="", encoding="utf-8") as f:
out = csv.writer(f)
out.writerow(["id", "item_group_id", "name", "finish", "description",
"link", "image_link", "additional_image_link", "price",
"stock", "brand", "category"])
for p in root.iter("product"):
images = [i.text for i in p.findall("images/image")]
for v in p.findall("variants/variant") or [p]:
price = v.find("price")
if price is None:
price = p.find("price")
out.writerow([
v.findtext("sku"), p.findtext("sku"), p.findtext("name"),
v.findtext("finish", ""), p.findtext("description"),
p.findtext("url"), images[0] if images else "",
",".join(images[1:]),
f"{price.text} {price.get('currency', '')}".strip(),
v.findtext("stock", p.findtext("stock", "")),
p.findtext("brand"), p.findtext("category/path"),
])The CSV that comes out, and the feed Textfinity builds from it
The script's output keeps the supplier's meaning but not yet the destination's spelling. Textfinity reads it, and the mapping renames and fixes each column: title combined from brand, name and finish, HTML removed from the description, the supplier link rewritten to your store, stock turned into Google's availability words. The download has the finished Google Shopping CSV for this product and two more.
supplier.csv (script output) → feed.csv (Textfinity output)
# supplier.csv (from the script)
id,item_group_id,name,finish,description,link,image_link,additional_image_link,price,stock,brand,category
LAMP-ARC-STEEL,LAMP-ARC,Arc Floor Lamp,Steel,"<p>Brushed steel arc lamp, 210 cm.</p>",https://supplier.example/p/lamp-arc,https://cdn.supplier.example/lamp-arc-a.jpg,https://cdn.supplier.example/lamp-arc-b.jpg,189.00 EUR,7,Lumen,Lighting > Floor lamps
LAMP-ARC-BRASS,LAMP-ARC,Arc Floor Lamp,Brass,"<p>Brushed steel arc lamp, 210 cm.</p>",https://supplier.example/p/lamp-arc,https://cdn.supplier.example/lamp-arc-a.jpg,https://cdn.supplier.example/lamp-arc-b.jpg,189.00 EUR,0,Lumen,Lighting > Floor lamps
# feed.csv (Textfinity, Google Shopping template)
id,item_group_id,title,description,link,image_link,additional_image_link,availability,price,brand,google_product_category
LAMP-ARC-STEEL,LAMP-ARC,"Lumen Arc Floor Lamp, Steel","Brushed steel arc lamp, 210 cm.",https://store.example/products/lamp-arc,https://cdn.supplier.example/lamp-arc-a.jpg,https://cdn.supplier.example/lamp-arc-b.jpg,in_stock,189.00 EUR,Lumen,Home & Garden > Lighting > Lamps
LAMP-ARC-BRASS,LAMP-ARC,"Lumen Arc Floor Lamp, Brass","Brushed steel arc lamp, 210 cm.",https://store.example/products/lamp-arc,https://cdn.supplier.example/lamp-arc-a.jpg,https://cdn.supplier.example/lamp-arc-b.jpg,out_of_stock,189.00 EUR,Lumen,Home & Garden > Lighting > LampsA conversion that reruns itself
A one-time converter produces a file that is stale by tomorrow. Supplier feeds change prices and stock daily, so the useful conversion is URL in, URL out, on a schedule. We add the XML URL (if it is Google-shaped) or the flattened CSV's URL as the source in Textfinity and map it once; every sync then re-pulls the source, rebuilds the feed (median rebuild 10.9 seconds on our platform) and serves it on the same URL. Merchant Center, Meta, a marketplace or your own store importer fetches the CSV from there.
Textfinity reads delimited text in any common flavour, so a TSV to CSV conversion is the same flow with nothing to configure: the delimiter is detected on import and the output is CSV, or TSV if the destination prefers tabs. It does not read JSON; a JSON catalog is flattened to CSV the same way as custom XML. And it does not write back into the supplier's system or import the CSV into your store for you; it publishes the file and the destination pulls it.
Examples
Worked examples, by situation
Three suppliers, three XML shapes, three destinations: one read directly, one Google feed reused for another channel, one flattened first.
Flat supplier RSS read as-is, with a margin
Before
<rss><channel> <item> <code>WB-500-GRN</code> <title>Water bottle 500ml green</title> <net_price>4.20</net_price> <qty>340</qty> </item> </channel></rss>
After
id,title,price,availability WB-500-GRN,"Trailmate Water Bottle 500 ml, Green",12.99 USD,in_stock
How we set it up
We add the supplier URL as an XML source; code, title, net_price and qty appear as columns because each item is flat. We map id as a rename from code, title as a combine of brand and title, price as a rename from retail_price, a column from a merged source (a retail price sheet joined on code), and availability with an advanced rule on qty. A filter "qty is more than 5" keeps thin stock out of ads.
Google RSS to Meta catalog columns
Before
<item> <g:id>TEE-ORG-NAVY-M</g:id> <g:availability>in_stock</g:availability> <g:price>29.00 USD</g:price> <g:item_group_id>TEE-ORG</g:item_group_id> </item>
After
id,availability,price,item_group_id,condition TEE-ORG-NAVY-M,in stock,29.00 USD,TEE-ORG,new
How we set it up
Textfinity reads the g: elements as columns. We build a Meta Ads feed, map availability with replace from regexp _ to a space and condition as the static value new; everything else is a rename. The Google XML feed keeps serving Google while the CSV serves Meta.
Nested supplier XML flattened on cron, then a custom column set
Before
<product> <sku>SOFA-3S-GREY</sku> <name>3-seat sofa</name> <brand>Harbor</brand> <color>Grey</color> <price currency="GBP">899</price> <images><image>https://cdn.example/sofa-grey.jpg</image></images> </product>
After
SKU,Name,Regular price,Images SOFA-3S-GREY,"Harbor 3-seat sofa, Grey",899,https://cdn.example/sofa-grey.jpg
How we set it up
The flattening script runs nightly and uploads supplier.csv to the shop's SFTP server, which we set up as the CSV source. We build a custom feed with the importer's headers as output columns, map Name with the advanced expression [brand] + " " + [name] + ", " + [color], and Regular price with replace from regexp " GBP$" to nothing, since the script wrote the currency into the value.
Copy this plan
Plan: XML URL to CSV URL
Five steps if the XML is Google-shaped; one more, run once and then on cron, if it is not.
01
Check the XML's shape
Open the file. RSS <item> or Atom <entry> elements with flat children read directly. A custom root, nested <images> or <variants>, or values held in attributes mean a flattening step: ask the supplier for a Google-format feed first, and script it only if they cannot.
02
Add the source
Send the Google-shaped XML URL, or the URL of the flattened CSV on your web or SFTP server, with login details if it is protected. Textfinity pulls the file and lists the columns it detects.
What we set up: We add the XML or CSV URL (HTTPS or SFTP) as the source in Textfinity.03
Read the profile
The feed hygiene profile shows which columns are empty, which values are inconsistent ("Green", "green", "GREEN") and which URLs are malformed, so you know what needs a rule before the first feed. A column you expected but cannot see was nested in the XML and skipped.
04
Create the destination feed
Pick a channel template (Google Shopping, Meta Ads, TikTok Ads, Pinterest Ads, Microsoft Ads, Amazon Ads and others) or Custom for your own header set, which is also how Idealo, Awin or a store importer get their columns.
What we set up: We build the feed from the channel template, or as a custom feed with your header set.05
Map columns and add the fixes
Rename for the one-to-one columns; combine, add suffix, remove from regexp and advanced for the rest; a Google Sheet lookup for categories, and a merged source for prices.
What we set up: We set each column's transform: rename, combine, add suffix, remove from regexp, advanced or replace from Google Sheet.06
Filter and publish
Exclude what the destination should not get, then give the destination the feed URL to fetch on a schedule.
What we set up: We add the filter rules and register the feed URL with you in the destination.
Customization tips
Make it yours
Ask for the Google feed before you write a script
Many suppliers and every major shop platform can already emit Google Shopping XML, often from a free module. That feed reads directly and needs no cron job of your own; the script is for suppliers who only offer their own schema.
Check the detected columns before you map
If a field does not show up as a column, it was nested or held in an attribute. Add it to the flattening step; do not assume a missing column is empty.
Keep the supplier's identifiers as id
Supplier codes are stable across feed versions; names and URLs are not. A changed id looks like a deleted product plus a new one to every channel and resets its history.
Add margin in a lookup, not in the XML
A merged source (a retail price sheet joined on sku) lets you keep retail prices in a sheet you control while the supplier's net price stays untouched in the source. When the markup is uniform, an advanced expression computes it: round(calc{[net_price] * 1.4}, 2).
Run the flattening before the shop sync
Schedule the script a little ahead of Textfinity's sync for that shop, and write to a temporary file that you rename when complete, so a sync never reads a half-written CSV.
Downloads
Take the files
FAQ
Questions about xml to csv product feed
How do I convert an XML product feed to CSV?
Treat each product or variant as a row and each field as a column, then rename the columns to what the destination expects. If the XML is Google-shaped (RSS items or Atom entries with flat children), Textfinity reads it by URL and does both on a schedule. If it nests images or variants or holds values in attributes, flatten it first with a short script into one row per variant and point Textfinity at that CSV.
Which XML formats does Textfinity read?
Google Shopping's shape only: RSS 2.0 (<rss><channel><item>) or Atom (<feed><entry>), one product per item, fields as flat child elements, with or without the g: prefix. Nested elements inside an item are skipped, and arbitrary schemas such as a Magento or PrestaShop custom XML export are not read; convert those to CSV or to Google-format XML first.
Can I convert XML to CSV online for free?
Generic converters flatten one level and are fine for a one-off look. For a feed that changes daily you want a conversion that reruns: Textfinity reads a Google-shaped XML or a CSV URL and republishes the mapped CSV on every sync, set up for you and quoted per catalog.
Does the converter handle TSV and JSON too?
TSV yes: Textfinity detects comma, tab, semicolon and pipe delimiters and outputs CSV or TSV, so a TSV to CSV conversion needs no mapping if the headers already match. JSON no: a JSON catalog has to be flattened to a CSV with one row per variant first, the same way as custom XML.
Can I import the converted CSV into Shopify or WooCommerce?
Textfinity publishes the CSV with whatever headers you choose via a Custom feed, including a store importer's column names, and you download or fetch it from the feed URL. It does not push products into the store itself; the import step is yours.
Set up for you
Have us convert your XML feed to CSV and keep it converted.
Send us a Google-format XML URL, or the URL of your flattened supplier CSV. We map the columns once in Textfinity, and the software rebuilds the feed on every sync at the same address; nothing to re-upload.
Tell us your store and channels. We reply within 24 hours.