How to Use International Sales & Currency in Etsy: Step-by-Step Tutorial
Updated October 2026
Introduction to International Sales & Currency Analysis for Etsy Sellers
If you ship outside your home country, your order file mixes currencies. Ten USD, ten GBP and ten EUR do not add up to thirty of anything, so the first rule of this analysis is: never add amounts that are in different currencies. This tutorial shows how to count orders and revenue per currency in the currency they were paid in, and then how to convert to one reporting currency, with the exchange rates you used written down, so that a country ranking can be checked.
All code uses pandas. The figures on this page come from a synthetic data set of 600 orders over 90 days that was generated for this tutorial, with synthetic exchange rates. It is not from a real shop, so your numbers will differ.
Prerequisites and Data Requirements
What You Need From Your Order Data
- An order identifier and sale date for each order.
- The ship-to country for each order.
- The currency the buyer paid in, and the order total in that currency.
- At least a few months of orders, so that a country with a handful of orders does not drive the ranking. How many months you need depends on how many orders per country you have, not on the calendar.
Exporting Your Etsy Orders
Etsy lets sellers download their sold orders as a CSV from Shop Manager. The menu path, the file name and the column names change over time, and we could not check Etsy's current help pages when updating this tutorial, so treat every Etsy-specific detail here as an assumption and check it against your own file. In particular, open the file and confirm that it has a column for the ship-to country, one for the currency and one for the order total, and check whether the total is in the buyer's currency or in your payout currency. If your file has only one of those, you cannot do the per-currency steps below.
Exchange Rates
You will need an exchange rate for each order's date. This tutorial uses the European Central Bank's euro foreign exchange reference rates: quoted for each currency against the euro and updated each working day at about 16:00 CET. The ECB publishes the reference rates for information purposes only and says that using them for transaction purposes is strongly discouraged, so treat the converted figures as a consistent yardstick for comparing countries, not as accounting values. For figures that match what Etsy paid out to you, use your payment account statements.
Step-by-Step Guide to Analyzing International Sales & Currency
Step 1: Load the Orders
The block below builds the synthetic orders. To use your own file, replace the generator with pd.read_csv and rename your columns to order_id, sale_date, ship_country, currency and order_total. Convert sale_date to a date with pd.to_datetime, and make sure the currency column holds three-letter ISO codes (USD, not $).
import numpy as np
import pandas as pd
# Synthetic orders generated for this tutorial. Replace this block with
# orders = pd.read_csv('your_etsy_orders.csv') and rename your columns to match.
rng = np.random.default_rng(7)
n = 600
mix = {'United States': ('USD', 0.50, 44), 'United Kingdom': ('GBP', 0.18, 36),
'Canada': ('CAD', 0.12, 58), 'Australia': ('AUD', 0.08, 70),
'Germany': ('EUR', 0.07, 38), 'France': ('EUR', 0.05, 40)}
countries = rng.choice(list(mix), size=n, p=[v[1] for v in mix.values()])
orders = pd.DataFrame({
'order_id': np.arange(10001, 10001 + n),
'sale_date': pd.Timestamp('2024-01-01') + pd.to_timedelta(rng.integers(0, 90, n), unit='D'),
'ship_country': countries,
'currency': [mix[c][0] for c in countries],
'order_total': [round(float(rng.gamma(4, mix[c][2] / 4)), 2) for c in countries],
})
orders = orders.sort_values(['sale_date', 'order_id']).reset_index(drop=True)
print(orders.head().to_string(index=False))
Expected output:
order_id sale_date ship_country currency order_total
10030 2024-01-01 United Kingdom GBP 46.90
10162 2024-01-01 Germany EUR 25.34
10532 2024-01-01 United States USD 51.62
10556 2024-01-01 United States USD 128.28
10019 2024-01-02 United Kingdom GBP 8.19
Step 2: Count Per Currency, in That Currency
Group by currency and report each currency's own totals. The revenue_local column is in the currency named in the row, so the five revenue figures cannot be added to each other. The order counts and shares of orders can be compared, because an order is one order whatever it was paid in.
# Per currency, in that currency. These amounts are NOT added across currencies.
by_cur = orders.groupby('currency').agg(
orders=('order_id', 'count'),
revenue_local=('order_total', 'sum'),
avg_order_local=('order_total', 'mean'),
).round(2)
by_cur['order_share_pct'] = (100 * by_cur['orders'] / by_cur['orders'].sum()).round(1)
print(by_cur.sort_values('orders', ascending=False))
Expected output:
orders revenue_local avg_order_local order_share_pct
currency
USD 288 12528.98 43.50 48.0
GBP 121 4208.53 34.78 20.2
CAD 72 4040.14 56.11 12.0
EUR 68 2567.92 37.76 11.3
AUD 51 3358.17 65.85 8.5
In this data set USD is 48.0% of orders and the four other currencies together are 52.0%. EUR covers two countries (Germany and France), so a currency table is not a country table. Step 3 is needed to rank countries by revenue.
Step 3: Convert to One Reporting Currency
To rank countries by revenue you need every order in one currency. The block below converts each order to USD with the rate from the order's date (the last published rate on or before that date, because the ECB publishes only on working days). It prints how many orders found no rate, which should be zero: if it is not, those orders are silently missing from every total that follows.
# Exchange rates in the layout of the ECB euro reference rates file: units of currency per 1 EUR, one row per day.
# These rates are synthetic. For your own file, download the full history CSV (eurofxref-hist.zip) from the ECB page.
days = pd.bdate_range('2023-12-25', '2024-03-31')
base = {'USD': 1.09, 'GBP': 0.86, 'CAD': 1.46, 'AUD': 1.64}
rates = pd.DataFrame({'Date': days})
for cur, r0 in base.items():
rates[cur] = r0 * np.exp(np.cumsum(rng.normal(0, 0.003, len(days))))
rates['EUR'] = 1.0
# The ECB publishes on working days only, so carry the last published rate over weekends.
daily = rates.set_index('Date').reindex(pd.date_range(days[0], orders['sale_date'].max(), freq='D')).ffill()
long = daily.stack()
key = pd.MultiIndex.from_arrays([orders['sale_date'], orders['currency']])
orders['cur_per_eur'] = long.reindex(key).to_numpy()
orders['usd_per_eur'] = daily['USD'].reindex(orders['sale_date']).to_numpy()
orders['order_usd'] = orders['order_total'] / orders['cur_per_eur'] * orders['usd_per_eur']
print('orders without a rate:', int(orders['order_usd'].isna().sum()))
print('total in USD at order-date rates:', round(orders['order_usd'].sum(), 2))
Expected output:
orders without a rate: 0
total in USD at order-date rates: 26219.63
Step 4: Rank Countries
Now group by ship-to country. The last line checks that the country totals add up to the currency totals, which is the reconciliation to run on your own file too: it fails if an order has a country but no currency, or the other way round.
by_country = orders.groupby('ship_country').agg(
currency=('currency', 'first'),
orders=('order_id', 'count'),
revenue_usd=('order_usd', 'sum'),
).sort_values('revenue_usd', ascending=False)
by_country['avg_order_usd'] = (by_country['revenue_usd'] / by_country['orders']).round(2)
by_country['share_pct'] = (100 * by_country['revenue_usd'] / by_country['revenue_usd'].sum()).round(1)
by_country['cumulative_pct'] = by_country['share_pct'].cumsum().round(1)
by_country['revenue_usd'] = by_country['revenue_usd'].round(2)
print(by_country.to_string())
by_currency_usd = orders.groupby('currency')['order_usd'].sum()
print('Sum by country equals sum by currency:', bool(np.isclose(by_country['revenue_usd'].sum(), by_currency_usd.sum(), atol=0.05)))
Expected output:
currency orders revenue_usd avg_order_usd share_pct cumulative_pct
ship_country
United States USD 288 12528.98 43.50 47.8 47.8
United Kingdom GBP 121 5516.15 45.59 21.0 68.8
Canada CAD 72 3043.90 42.28 11.6 80.4
Australia AUD 51 2280.60 44.72 8.7 89.1
Germany EUR 42 1807.73 43.04 6.9 96.0
France EUR 26 1042.27 40.09 4.0 100.0
Sum by country equals sum by currency: True
Reading the table: the United States is 47.8% of converted revenue and the top three countries (United States, United Kingdom, Canada) are 80.4% of it, from the cumulative column. The United Kingdom has the highest average order value in this data set (45.59 USD, against 43.50 for the United States). The table does not say why, and with 121 UK orders a difference of a couple of dollars in the average is small next to the spread of individual order values, so do not set prices from it. Germany and France are both paid in EUR; their two revenue figures add up to the EUR figure in Step 5 (1,807.73 + 1,042.27 = 2,850.00).
Step 5: Measure What the Exchange Rate Did to the Converted Figure
A converted total moves when the rate moves, even if buyers paid exactly the same. To see how much, convert the same local amounts twice, once at each order's own date rate and once at the last rate in the file, and compare.
# How much does the exchange rate move the USD figure? Same local amounts, two rates.
last = rates.drop(columns='Date').iloc[-1].astype(float)
orders['order_usd_at_last_rate'] = orders['order_total'] / orders['currency'].map(last) * last['USD']
fx = orders.groupby('currency')[['order_usd', 'order_usd_at_last_rate']].sum().round(2)
fx['difference_usd'] = (fx['order_usd_at_last_rate'] - fx['order_usd']).round(2)
fx['difference_pct'] = (100 * fx['difference_usd'] / fx['order_usd']).round(1)
print(fx)
Expected output:
order_usd order_usd_at_last_rate difference_usd difference_pct
currency
AUD 2280.60 2328.86 48.26 2.1
CAD 3043.90 3088.55 44.65 1.5
EUR 2850.00 2882.34 32.34 1.1
GBP 5516.15 5649.73 133.58 2.4
USD 12528.98 12528.98 0.00 0.0
The USD row is zero because USD orders do not change when converted to USD. For the other currencies, the difference is 1.1% to 2.4% of the converted figure in this synthetic data, and it depends entirely on the synthetic rate path. It says how sensitive the converted number is to the choice of rate, not what your profit would be: to measure the effect on profit you also need your costs and the rate at which your payments were actually converted.
Step 6: Use the Results Carefully
Some uses that follow from the tables, each of which is a hypothesis to test rather than a conclusion:
- Shipping: if three countries account for most converted revenue, as the top three do in the synthetic example, a shipping profile for each is worth looking at. Whether faster shipping would raise sales there is not something this table can show.
- Pricing by market: if you test different prices in different markets, judge the result with a proper comparison, in the way A/B testing compares variants, and not by comparing two averages.
- Seasonality: compare the same months across years rather than consecutive quarters, if you have the history.
Checking Your Own Numbers
Reconcile Order Counts
The number of rows in your file for the period should equal the number of orders you see in Etsy Shop Manager for the same dates, unless you removed cancelled orders on purpose. Check where your own export puts refunds and cancellations before comparing.
Reconcile Revenue
Per currency, the sum of order_total in your analysis should equal the sum of the same column in the file you uploaded. After conversion, expect your total to differ from your payout statements, because Etsy and your payment provider convert at their own rates and on their own dates and deduct fees and refunds. There is no standard tolerance for that gap; it is whatever your statements say it is.
Top Country Sanity Check
If an unexpected country is at the top, look for a few very large orders, a mislabeled country, or a package forwarding address.
Run It on Your Own Etsy Export
Export your orders from Etsy as a CSV and upload it at MCP Analytics. You can group revenue by country and by currency on your file and see the result with the method used. You choose the columns; nothing connects to your Etsy account.
Troubleshooting Common Issues
Issue 1: Missing or Blank Country Values
Count them before grouping, because groupby drops rows whose key is missing.
print('orders with no ship country:', int(orders['ship_country'].isna().sum()))
print('orders with no currency:', int(orders['currency'].isna().sum()))
If either count is not zero, fix the source file or report those orders as their own line, so that totals still match the file.
Issue 2: Orders Without a Rate
The Step 3 check prints the number of orders with no rate. The usual causes are a date before the first date in your rate file, a currency the ECB does not quote, or a date format that did not parse.
Issue 3: Converted Totals Differ From Your Payouts
Expected: see Step 5 and the ECB caveat above. For figures that reconcile to money received, use the amounts on your payment account statements and compare in the currency you are paid in.
Issue 4: No Clear Geographic Pattern
If revenue is spread thinly across many countries, extend the period, or rank by order count instead of revenue, or group countries into regions. A flat table is a result too.
Conclusion: What This Analysis Does and Does Not Show
You now have a per-currency table that never adds unlike currencies, a country ranking in one reporting currency with the rate source stated, a reconciliation between the two, and a measure of how much the rate choice moves the converted figure. What it does not show is why a market performs as it does, or what would happen if you changed shipping or prices there. Those need a comparison, not a ranking.
Explore more: Etsy Shop Analytics: all tools, tutorials, and guides →
Not sure which plan? Compare plans →