It occurred to me I’m not always putting my R powers to good use. So why not make a wrapper to a data API everyone can use? I’ve decided to use my relatively deep knowledge of ebay, and wrap its search API (a.k.a the Finding API) using the httr package, and here we are :)

search_ebay("elvis costume")
## # A tibble: 10 x 17
##    itemId title categoryId categoryName viewItemURL location sheepingType shipToLocations isMultiVariatio~
##    <chr>  <chr>      <dbl> <chr>        <chr>       <chr>    <chr>        <chr>           <lgl>           
##  1 36160~ Elvi~      52762 Men          http://www~ Travers~ Free         Worldwide       TRUE            
##  2 22275~ ELVI~      52762 Men          http://www~ Barring~ Calculated   Worldwide       FALSE           
##  3 26320~ Adul~      52762 Men          http://www~ Tonawan~ Free         Worldwide       TRUE            
##  4 12265~ Rubi~      52762 Men          http://www~ USA      Free         Worldwide       FALSE           
##  5 13243~ "Elv~      52762 Men          http://www~ Florenc~ Flat         Worldwide       FALSE           
##  6 28276~ ELVI~      52762 Men          http://www~ Las Veg~ Free         Worldwide       FALSE           
##  7 28226~ Elvi~      80913 Boys         http://www~ Chicago~ Free         Worldwide       FALSE           
##  8 28276~ ELVI~      52762 Men          http://www~ Las Veg~ Free         Worldwide       FALSE           
##  9 35221~ Elvi~      52762 Men          http://www~ Bakersf~ Free         Worldwide       FALSE           
## 10 22267~ Elvi~      52762 Men          http://www~ Clarenc~ Calculated   Worldwide       FALSE           
## # ... with 8 more variables: conditionId <dbl>, conditionName <chr>, listingType <chr>, startTime <date>,
## #   endTime <date>, watchCount <dbl>, price <dbl>, currency <chr>

You can get the package here. The first part of this post will expand on its README, detailing some more stuff you should know (though I can’t get into every single detail, the ebay ecosystem is massive). In the second part I’ll give you some ideas of what you can do with this package.

Search It For A Spin

Install it from my Github repo:


Load it:

## Welcome to the R wrapper to the ebay Finding API.
## I see you don't have a token for this API (environment variable 'EBAY_TOK').
## Please get one at and set it using setEbayToken(YOUR_TOKEN).

You should see a message saying you have not set a your ebay token yet. You need to register to the ebay Developers Program here and get a token for the Finding API, for Production (not Sandbox). It takes 2 minutes. Save the token somewhere for later use and set it so that R can recognize it (I’m setting an enviroment variable here so you don’t have to input it to the function each time, though it’s a possibility):


Now Search, I said, Search!

You can search using keywords like above. You can search in a specific category or categories (a.k.a Browse):

search_ebay(categoryName = "Video Game Consoles")
## # A tibble: 10 x 17
##    itemId title categoryId categoryName viewItemURL location sheepingType shipToLocations isMultiVariatio~
##    <chr>  <chr>      <dbl> <chr>        <chr>       <chr>    <chr>        <chr>           <lgl>           
##  1 19237~ Micr~     139971 Video Game ~ http://www~ USA      Free         Worldwide       FALSE           
##  2 33204~ Sony~     139971 Video Game ~ http://www~ Forest ~ Free         Worldwide       FALSE           
##  3 17303~ Nint~     139971 Video Game ~ http://www~ Grovepo~ Flat         Worldwide       FALSE           
##  4 18289~ Sony~     139971 Video Game ~ http://www~ Burbank~ Free         Worldwide       FALSE           
##  5 20213~ RETR~     139971 Video Game ~ http://www~ Bensale~ Free         Worldwide       FALSE           
##  6 12286~ Micr~     139971 Video Game ~ http://www~ USA      Flat         Worldwide       FALSE           
##  7 31198~ Sony~     139971 Video Game ~ http://www~ USA      Free         Worldwide       FALSE           
##  8 32279~ Supe~     139971 Video Game ~ http://www~ Dearbor~ Free         Worldwide       FALSE           
##  9 29235~ Micr~     139971 Video Game ~ http://www~ Atlanta~ Free         Worldwide       FALSE           
## 10 29233~ Play~     139971 Video Game ~ http://www~ USA      Free         Worldwide       FALSE           
## # ... with 8 more variables: conditionId <dbl>, conditionName <chr>, listingType <chr>, startTime <date>,
## #   endTime <date>, watchCount <dbl>, price <dbl>, currency <chr>

You can search by both keywords and category, e.g. you want to see the game Yahtzee in both actual games and in non-fiction books, like manuals:

search_ebay("Yahtzee", categoryName = c("Nonfiction", "Board & Traditional Games"))
## # A tibble: 10 x 17
##    itemId title categoryId categoryName viewItemURL location sheepingType shipToLocations isMultiVariatio~
##    <chr>  <chr>      <dbl> <chr>        <chr>       <chr>    <chr>        <chr>           <lgl>           
##  1 20162~ 250 ~     180349 Contemporar~ http://www~ Wellsto~ Free         Worldwide       FALSE           
##  2 20186~ " SA~     180349 Contemporar~ http://www~ Wellsto~ Flat         Worldwide       FALSE           
##  3 32185~ Yaht~     180349 Contemporar~ http://www~ USA      Free         Worldwide       FALSE           
##  4 20191~ LOWE~     180349 Contemporar~ http://www~ Wellsto~ Free         Worldwide       FALSE           
##  5 22238~ Yaht~     180349 Contemporar~ http://www~ Pasaden~ Free         Worldwide       FALSE           
##  6 16246~ Back~     180349 Contemporar~ http://www~ Hazleto~ Free         Worldwide       FALSE           
##  7 28277~ New ~     180349 Contemporar~ http://www~ North L~ Free         Worldwide       FALSE           
##  8 32265~ Yaht~     180349 Contemporar~ http://www~ USA      Free         Worldwide       FALSE           
##  9 32292~ Yaht~     180349 Contemporar~ http://www~ USA      Free         Worldwide       FALSE           
## 10 20176~ 50 T~     180349 Contemporar~ http://www~ Wellsto~ Free         Worldwide       FALSE           
## # ... with 8 more variables: conditionId <dbl>, conditionName <chr>, listingType <chr>, startTime <date>,
## #   endTime <date>, watchCount <dbl>, price <dbl>, currency <chr>

But this is kids stuff. So far we have accepted all of search_ebay’s defaults. Let’s search the UK site for new large size Fruit of the Loom women’s T-shirts from top rated sellers at a max price of 10 pounds!

res <- search_ebay("women's t-shirts",
                   site = "UK",
                   condition = "New",
                   listingType = "FixedPrice",
                   topRatedSellerOnly = TRUE,
                   priceRange = c(0, 10),
                   aspectFilter = list(
                     Brand = "Fruit of the Loom",
                     Size = "L"

res[, c("title", "price", "currency")]
## # A tibble: 10 x 3
##    title                                                                              price currency
##    <chr>                                                                              <dbl> <chr>   
##  1 Mens Ladies Womens Novelty Print TShirt Funny Tee Rude Joke Xmas Top Gift Unisex    4.99 GBP     
##  3 LADIES 100% COTTON T-SHIRT - FRUIT of the LOOM PLAIN T SHIRT Womens Female          2.99 GBP     
##  4 Fruit of the Loom 100% Cotton Plain Blank Men's Women's Tee Shirt Tshirt T-Shirt    3.25 GBP     
##  5 Men's Women's Fruit of the Loom Plain 100% Cotton Blank Tee Shirt Tshirt T-Shirt    2.95 GBP     
##  7 Fruit of the Loom Long Sleeve 100% Cotton Plain Blank Men's Women's Tee Shirt's     4.08 GBP     
##  8 madness t-shirt retro style men's women's sizes                                     6    GBP     
##  9 Fruit of the Loom 100% Cotton Plain Blank Men's Women's T-Shirts Value Weight       3.57 GBP     


So, I think most of what you should know you can read in the very detailed search_ebay function (do ?search_ebay). There are a few things worth emphasizing before you continue:

  • The ebayr search_ebay function currently wraps only the Finding API and specifically the “findItemsAdvanced” call. This means you can only search for items which are live on site. If there’s a requirement I might also wrap the “findItemsComplete” call which allows you to dig in the past and find items which are no longer alive, maybe because they were sold. For more documentation of the original API see the API Reference.
  • Whatever you do, make sure you adhere to ebay’s API license guidelines. If you abuse this API, ebay could easily block all users of this package.
  • Speaking of abuse, there is a rate limit to the API of 5,000 requests per day. This is for your own good. And please notice that although you can specify nResults = 10000 to get 10K items1, the maximum results per request or page is 100. The function will simply take care of the pagination for you, calling the API 100 times.
  • Currently only searching in the US, UK, German and Australian sites is possible.
  • By default the function will return the items tibble, which will hopefully contain the relevant items with a few attributes (see below). If you specify returnAll = TRUE you can get a more detailed instance of a R6 EbayResponse class, holding also the status code for the httr::GET request and more. See ?search_ebay.
  • The API does not actually search by category name, which is not unique (e.g. there is more than a single category on the ebay US site named “Digital Cameras”!). It searches by category ID, which is unique. The categoryName parameter is there for casual beginner users, but once you gain experience you should probably specify one or more categories through the categoryId parameter. If you stick to categoryName and more than a single categoryID belongs to each of the names specified, the function will warn you this happened but will continue regardless (up to a maximum of 3 categories allowed by the API). If no category is found whose name matches the input exactly, the function will suggest a few similar ones, try and pick one. Where to find the category name or ID?
    • through the ebay site (in an item’s page, in the URL)
    • through previous results in the items tibble
    • or you might not need it, as the ebay search is quite good (e.g. shouldn’t get results for “Nikon DSLR” in “Women’s Handbags”)
  • aspectFilter: this parameter allows you to further refine your results, e.g. specifying aspectFilter = list("Storage Capacity" = "64GB") when searching for “iPhone X”, will only return iPhones with the specified Storage Capacity. Naturally these possible name-values differ from category to category and currently the best place to look for them is by looking at the left side bar in the ebay search results page. Only one “value” per “name” is supported in this function, and the “name” changes according to the ebay site you’re searching in. For example to look for black shirts in the US site you specify aspectFilter = list(Color = "Black"), in UK aspectFilter = list("Main Colour" = "Black") and in the German site (“DE”): aspectFilter = list(Farbe = "Schwarz").
  • ebay started as an auction site, but today most items have a fixed price. There are still auction items, whose price may not be determined, and you should be aware of that, specifying only listingType = FixedPrice if that’s what you’re after.
  • Speaking of price, you should be careful to look at the currency of the price in the items tibble, this needs not be in USD automatically even when searching the US site!

There is more to know. See ?search_ebay and the API documentation.

But let’s also look at what is returned in the items tibble:

  • itemId ebay’s unique item ID for each item
  • title item’s title as it appears on site
  • categoryId item’s primary category ID, note that an item can have a secondary category, not returned here, and you could search for category 1 and get also some results from category 2 because of this.
  • categoryName category’s name
  • viewItemURL item’s URL on ebay
  • location a string specifying the item’s location, e.g. “Melbourne, Australia”
  • shippingType free or some other values, see API Reference
  • shipToLocations Worldwide or other values, see API Reference
  • isMultiVariationListing see details on the hideDuplicateItems parameter
  • conditionId ebay’s ID for a condition, e.g. “1000” for “New”
  • conditionName name of condition, e.g. “New”, “Used”
  • listingType see details on the listingType parameter
  • startTime the time the item successfully uploaded to site
  • endTime the time the item is scheduled to end, this could be sooner if it sells
  • watchCount no. of users watching the item in their watchlist, could be NA for some reason, maybe when it’s zero
  • price current price of the item in the site’s currency, notice what this means for auction items
  • currency the price currency, e.g. “AUD” for Australian Dollars

Browse Inspiration

A T-Shirt cost vs. Brand

A T-shirt. For women. Size Large. How many results? (Here I use the fact I already know the category ID, see above, it’s not complicated)

res <- search_ebay(categoryId = 63869, aspectFilter = list("Size (Women's)" = "L"),
                   returnAll = TRUE,
                   verbose = FALSE)
## [1] 1343681

1.3 million results. Yes, that’s ebay for you.

Let’s further limit our results by asking for fixed price items only, new items only, exclude any items with currency other than USD or items not located in the US.

Let’s look at the distribution of price for these T-shirts, by some popular brands, getting the first 100 shirts by “Best Match”, for each brand:


brands <- c(
  'Brisco Brands',
  'Next Level',
  'DC Comics',
  'Fruit of the Loom',
  'Bella + Canvas',
  'Harry Potter',
  'Victoria\'s Secret',

getPricesForBrand <- function(brand) {
  res <- search_ebay(categoryId = 63869,
                   listingType = "FixedPrice", condition = "New",
                   aspectFilter = list("Size (Women's)" = "L",
                                       Brand = brand),
                   nResults = 100, verbose = FALSE)
  res %>%
    mutate(isUS = stringr::str_detect(location, "USA")) %>%
    filter(currency == "USD", isUS == TRUE) %>%
    select(price) %>%
    unlist() %>%

tibble(brand = brands) %>%
  mutate(price = map(brands, getPricesForBrand)) %>%
  unnest(price) %>%
  group_by(brand) %>%
  mutate(medianPrice = median(price)) %>%
  ggplot(aes(x = price, y = reorder(brand, -medianPrice), fill = ..x..)) +
  geom_density_ridges_gradient(rel_min_height = 0.0) +
  scale_fill_viridis(name = "Price USD", option = "C", labels = dollar_format()) +
  theme_ridges(font_size = 12, grid = TRUE) +
  theme(axis.title.y = element_blank(),
        axis.title.x = element_blank(),
        text = element_text(family="mono"),
        axis.text.y = element_text(size = 9)) +
  labs(title = "Price of Women's Large Size T-Shirts by Brand on ebay",
       subtitle = "Obtained with the ebayr package, 2017/12/19") +
  scale_x_continuous(limits = c(0, 60), labels = dollar_format())

USA vs. China

Ever get the feeling stuff from China costs much (much much) less than in the US? Let’s look for some stuff and compare. We’ll make sure we have at least 20 items for the same keywords from each country and look at the medians:


stuff <- c(
  "hulk costume",
  "elsa costume",
  "wall painting",
  "dog sweater",
  "wireless earphones",
  "ice pick",
  "iphone case",
  "256 GB memory card",
  "cocktail dress",
  "wedding ring",
  "elephant doll",
  "cookie cutter",
  "tatoo ink",
  "disco ball",
  "measuring spoon"

decideLocation <- function(location) {
  if (stringr::str_detect(location, "USA")) {
  } else if (stringr::str_detect(location, "China")) {
  } else {

getPricesForThingInUSChina <- function(thing, minSampleSize = 20, .nResults = 1000) {
  res <- search_ebay(thing,
                   listingType = "FixedPrice", condition = "New",
                   nResults = .nResults, verbose = FALSE)
  summaryRes <- res %>%
    mutate(location2 = map_chr(location, decideLocation)) %>%
    filter(currency == "USD", location2 %in% c("USA", "China")) %>%
    group_by(location2) %>%
    summarise(n = n(), medianPrice = median(price)) %>%
    mutate(sampleLargeEnough = n >= minSampleSize)
  if (nrow(summaryRes) == 2 && all(summaryRes$sampleLargeEnough)) {
    return(list(China = unlist(summaryRes[
      summaryRes$location2 == "China",
      USA = unlist(summaryRes[
      summaryRes$location2 == "USA",
  } else {

tibble(thing = stuff) %>%
  mutate(price = map(thing, getPricesForThingInUSChina)) %>%
  bind_cols(bind_rows(res$price)) %>%
    ggplot(aes(x = China, xend = USA, y = reorder(thing, -USA), group = thing)) + 
    geom_dumbbell(color = "grey", 
                  colour_x = "indianred1",
                  colour_xend = "#0e668b",
                  size = 1,
                  size_x = 2,
                  size_xend = 2) + 
    scale_x_continuous(label = dollar_format(),
                       breaks = seq(0, 150, 30)) + 
    labs(title = "Median Price of Stuff on ebay, China vs. US",
         subtitle = "Obtained with the ebayr package, 2017/12/19, China in Red",
         x = "",
         y = "") +
    theme(plot.title = element_text(face = "bold"),
          text = element_text(family="mono"),
          axis.text.y = element_text(size = 9),
          axis.text.x = element_text(size = 12),
          panel.grid.major.x=element_line(color = "grey"),

Jeans in the USA

Finally, let’s map the price of used women’s jeans on the USA map. We’ll make sure we have at least 10 items for each state and look at the medians:


getLocationUSState <- function(location) {
  stateIdx <- which(str_detect(location, str_c(",",, ",")))
  if (length(stateIdx) != 1) {
  } else {

res <- search_ebay(categoryId = 11554, condition = "Used", listingType = "FixedPrice",
                   nResults = 5000, verbose = FALSE)

medianByState <- res %>%
  filter(currency == "USD") %>%
  mutate(region = map_chr(location, getLocationUSState)) %>%
  group_by(region) %>%
  summarise(n = n(), medianPrice = median(price)) %>%
  filter(n >= 10) %>%
  na.omit() %>%
  right_join(tibble(region = c(str_to_lower(, "district of columbia")), "region")

ggplot(data = map_data("state") %>% inner_join(medianByState)) +
  geom_polygon(aes(x = long, y = lat, fill = medianPrice, group = group), color = "white") + 
  coord_fixed(1.3) +
  labs(title = "Median Price of Used Women's Jeans on ebay by State",
       subtitle = "Obtained with the ebayr package, 2017/12/19, at least 10 items per state",
       x = "",
       y = "") +
  theme(plot.title = element_text(face = "bold"),
        text = element_text(family="mono"),
        axis.text.y = element_blank(),
        axis.text.x = element_blank(),
        axis.ticks=element_blank()) +
  scale_fill_continuous(name = "Price USD", labels = dollar_format())

Let’s see the top and bottom states:

medianByState %>%
    arrange(-medianPrice) %>%
    slice(c(1:5, 39:43))
## # A tibble: 10 x 3
##    region             n medianPrice
##    <chr>          <int>       <dbl>
##  1 new york         121        30.0
##  2 utah              43        29.8
##  3 california       502        29.2
##  4 wisconsin         59        29.0
##  5 florida          402        28.1
##  6 north carolina   142        18.0
##  7 maryland          47        17.8
##  8 iowa             656        16.2
##  9 delaware          15        15.7
## 10 missouri          81        15.4

So the price for used women’s jeans in the state of New York is about twice the price in Missouri.

Remember Kids

Again I’d like to emphasize the importance of adhering to to ebay’s API license guidelines. You are now able to programmatically search through ebay! Hurray ebay, hurray httr, hurray ebayr.

  1. Why would you want that?