Reversing API Endpoints from Barcoo App

updated 9 min read #tutorial #reverse engineering

Update 2026: I re-checked the endpoints against the current app. The product endpoint still returns the same JSON, but only with the cookies the app sends, and the data looks like an old cache. See Revisited in 2026.

Introduction#

This post shows how I found the product API behind the barcoo app by decompiling its Android version. It is for anyone who wants to look up product data by EAN, or wants to see how to find the API of an Android app.

Barcode scanner apps let you look up product details of food, cosmetics and so on while shopping. A well-known one in German-speaking countries is barcoo. I wanted a fixed scanner for my fridge that reads a product’s EAN and stores the product in a database, so I needed an API that provides this data. There are large EAN product databases, but they cost money. So I had the idea of using the API of an existing barcode scanner app.

I had already analysed this app’s endpoints in an earlier experiment, with a jailbroken iPad Air, SSL Kill Switch 2 and mitmproxy. This time I wanted to try a decompiled Android version of the app.

The app’s description on barcoo - QR & Barcode Scanner on Google Play:

With the barcoo app, you can easily find out which substances a product contains while shopping. All you have to do is scan the barcode on the packaging. This way you get information about the product, the price, where you can buy the product nearby and sometimes even test reports. barcoo scanner recognizes any QR code, barcode as well as Datamatrix, EAN and ISBN that your camera has captured.

Reverse engineering#

An APK file holds an app’s code, resources, assets, certificates and manifest. barcoo is distributed as a split APK, a format that puts hardware-dependent parts into separate APK files. This is a common way to keep the installation small.

File manager showing the downloaded barcoo v43 split APK: config.arm64_v8a.apk, config.xxhdpi.apk, de.barcoo.android.apk, icon.png and manifest.json
Extracted split APK

Here, de.barcoo.android.apk is the base APK. It contains the code and resources that all other split APKs use and provides the app’s basic functionality, so that’s the one to examine.

Android apps are written in Java or Kotlin and compiled to JVM bytecode. The .class files are then converted into .dex (Dalvik Executable) files, a compact format for devices with limited memory and processing power.

Contents of the extracted base APK: folders such as assets, kotlin, META-INF, okhttp3 and res, AndroidManifest.xml and the files classes.dex, classes2.dex and classes3.dex
Extracted base APK

The extracted base APK contains three .dex files. We need to decompile them to understand how the code works. I achieved this using skylot/jadx, a CLI and GUI tool for producing Java source code from Android Dex and APK files.

To cut a long story short: classes2.dex contains all the relevant API logic. The source folders follow the reverse DNS scheme, and the company behind barcoo is called Offerista, so the right folder was easy to find.

The following screenshot shows the decompiled Java code in sources/com/offerista/android/misc:

Decompiled Java files in sources/com/offerista/android/misc, including AppSettings.java
Decompiled classes2.dex

The app probably talks to a server at barcoo.com, and a simple grep -Ri barcoo.com turned out to be very effective:

# grep -Ri barcoo.com
misc/AppSettings.java:    public static final String BASE_URL_PRODUCTION = "https://www.barcoo.com";
misc/Settings.java:        arrayMap.put(GEOSERVER_DEFINITIONS_BASE, "https://geoserver.barcoo.com");
activity/SearchActivity.java:                                AutoCompleteAdapter.this.conn = new URL("http://www.barcoo.com/search/suggest?spellcheck.q=" + URLEncoder.encode(charSequence.toString(), "UTF-8")).openConnection();

Voilà, we have already found BASE_URL_PRODUCTION and the first API endpoint.

  • BASE_URL_PRODUCTION = "https://www.barcoo.com"
  • Endpoint 1 (returns JSON): http://www.barcoo.com/search/suggest?spellcheck.q=

The app apparently uses this endpoint for search suggestions and autocorrection in its free-text search.

Here is an example request with the keyword “milram”:

# curl -Ls http://www.barcoo.com/search/suggest?spellcheck.q=milram | jq
{
  "responseHeader": {
    "status": 0,
    "QTime": 2
  },
  "spellcheck": {
    "suggestions": [
      "milram",
      {
        "numFound": 352,
        "startOffset": 0,
        "endOffset": 6,
        "suggestion": [
          "milram",
          "milram bu",
          "milram burlander",
          "milram fruchtbumi",
          "milram fruchtbumi drink"
        ]
      }
    ]
  }
}

Next, the AppSettings class in misc/AppSettings.java:

public final class AppSettings {
    public static final String BASE_URL_PRODUCTION = "https://www.barcoo.com";
    private static final String EAN_PATH = "/product/psp?m=android#!product/__EAN__/EAN/DE/source=__SOURCE__";
    private static final String PRODUCT_BROWSER_SEARCH_PATH = "/produkt-suche#!suche/";

Besides the base URL, it contains a hint to the endpoint we are actually looking for: one that takes an EAN and returns information about the product.

  • Endpoint 2 (returns HTML): https://www.barcoo.com/product/psp?m=android#!product/__EAN__/EAN/DE/source=__SOURCE__
  • __EAN__ is a string transmitted by the barcode reader
  • __SOURCE__ is still unknown (wanted!)

Further down in the file, getRelativeSearchUrl() uses EAN_PATH. It takes two strings: the __EAN__ value and the __SOURCE__ value we are looking for.

public String getRelativeSearchUrl(String str, String str2) {
        if (str2 == null) {
            str2 = com.appsflyer.oaid.BuildConfig.FLAVOR;
        }
        if (str == null) {
            str = com.appsflyer.oaid.BuildConfig.FLAVOR;
        }
        if (BarcodeUtil.checkBarcodeType(str) != 0) {
            try {
                return EAN_PATH.replace("__EAN__", URLEncoder.encode(str, "UTF-8")).replace("__SOURCE__", URLEncoder.encode(str2, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                return com.appsflyer.oaid.BuildConfig.FLAVOR;
            }
        } else {
            try {
                return PRODUCT_BROWSER_SEARCH_PATH + URLEncoder.encode(str, "UTF-8").replace("+", "%20");
            } catch (UnsupportedEncodingException e2) {
                e2.printStackTrace();
                return com.appsflyer.oaid.BuildConfig.FLAVOR;
            }
        }
    }

Let’s see where this function is called and where the two parameters come from:

# grep -Ri getRelativeSearchUrl
startup/ActivityLauncher.java:        String relativeSearchUrl = this.appSettings.getRelativeSearchUrl(str, "textsearch");
misc/AppSettings.java:    public String getRelativeSearchUrl(String str, String str2) {

The answer is in startup/ActivityLauncher.java:

    public Launch barcooWebViewActivityForSearchResult(String str) {
        String relativeSearchUrl = this.appSettings.getRelativeSearchUrl(str, "textsearch");
        if (relativeSearchUrl == null || relativeSearchUrl.length() == 0) {
            return null;
        }

The mystery is solved, at least for text search: this call passes __SOURCE__ = "textsearch".

Now we can assemble an example request.

For __EAN__ we use 4036300107756, a cheesy example product.

https://www.barcoo.com/product/psp?m=android#!product/4036300107756/EAN/DE/source=textsearch

Note: the source parameter seems to be optional.

EAN Endpoint (returns HTML): https://www.barcoo.com/product/psp?m=android#!product/__EAN__/EAN/DE

The iOS version calls the JSON endpoint directly and renders the interface natively. On Android, the developers took a shortcut and show the product information in a WebView. Luckily, the web page behind it calls exactly this JSON endpoint from JavaScript.

The Firefox developer tools show the network requests:

Firefox network monitor on the barcoo product page for EAN 4036300107756, with the JSON response for Milram Burlander opened
API response

EAN API endpoint#

The final API request looks like this:

https://www.barcoo.com/product_api/DE/EAN/__EAN__

It returns valid JSON, so we have found the endpoint we wanted. 🎉

# curl -s https://www.barcoo.com/product_api/DE/EAN/4036300107756 | jq '.product'
{
  "pi": "4036300107756",
  "pins": "EAN",
  "c": "DE",
  "non_unique": false,
  "title": "Milram Burlander",
  "sub_title": "DMK Deutsches Milchkontor GmbH",
  "fallback_title": "Milram Burlander 150 gr",
  "category_name": "Schnittkäse",
  "category_key": "CUT_CHEESE",
...

Revisited in 2026#

Four years later I wanted to know whether my fridge scanner could still talk to barcoo. The short version: the endpoint from this post still exists and still returns the same JSON. A plain curl no longer works, though. Without the cookies the app sends, the server answers with 404 Not Found, and that looks exactly like the endpoint is gone.

What I checked#

I took the current app from the Play Store listing: barcoo version 77 (versionCode 25114, built 2026-09-08), targetSdk 36. It’s still a split APK (base APK plus config.arm64_v8a and config.mdpi), and the base APK now has three .dex files. I decompiled it again with jadx. All the analysis was static: I didn’t install or run the app.

The base URL hasn’t changed. It just moved from misc/AppSettings.java into a shared module:

# grep -rn 'barcoo.com' sources/com/shared sources/com/offerista
com/shared/misc/utils/CommonUtils.java:    public static final String BASE_URL_PRODUCTION = "https://www.barcoo.com";
com/shared/misc/Settings.java:        arrayMap.put(GEOSERVER_DEFINITIONS_BASE, "https://geoserver.barcoo.com");
com/offerista/android/activity/SearchActivity.java:  ... new URL("https://www.barcoo.com/search/suggest?spellcheck.q=" + URLEncoder.encode(...))
com/checkitmobile/cimTracker/CimTrackerEventClient.java:    private static final String BASE_URL_TRACKING = "https://track.barcoo.com";

The suggestion endpoint is still there, now over https. It still answers without any special headers:

# curl -s 'https://www.barcoo.com/search/suggest?spellcheck.q=milram' | jq -c
{"responseHeader":{"status":0,"QTime":1},"spellcheck":{"suggestions":["milram",{"numFound":353,"startOffset":0,"endOffset":6,"suggestion":["milram","milram bu","milram burlander","milram fruchtbumi drink","milram fruchtbumi"]}]}}

Scanning a barcode no longer goes through a WebView#

In 2022 I found the product page as a WebView URL (/product/psp?m=android#!product/__EAN__/...). That URL is still in AppSettings, but the app now only uses it for free-text search results (source=textsearch). A scanned EAN takes a different, native path:

// com/offerista/android/scan/ProductResultHandler.java
Intent intent = new Intent(getActivity(), ProductSummaryActivity.class);
intent.putExtra(ProductSummaryActivity.ARG_PI, displayResult);   // the scanned EAN
intent.putExtra(ProductSummaryActivity.ARG_PINS, "EAN");
intent.putExtra(ProductSummaryActivity.ARG_COUNTRY, BuildConfig.COUNTRY_CODE); // "DE"

ProductSummaryViewModel then calls a Retrofit interface, and the URL there is the one I found in the browser back then:

// com/shared/rest/CimService.java
@GET("/product_api/{country}/{pins}/{pi}")
Single<ProductSummary.Entity> getProduct(@Path("pi") String str, @Path("pins") String str2, @Path("country") String str3);

The Retrofit base URL is AppSettings.getBaseUrl(), which defaults to https://www.barcoo.com. So the request is still GET https://www.barcoo.com/product_api/DE/EAN/<ean>.

The missing piece: cookies#

The interesting part is the OkHttp client. There’s no API key and no signature on this request. UserAgentInterceptor sets User-Agent: barcoo/77. CimInterceptor adds Accept-Language and turns every cookie in the app’s cookie store into its own Cookie header. Those cookies come from AppSettings.currentCookieValues():

// com/offerista/android/misc/AppSettings.java
arrayMap.put("platform", "android");
arrayMap.put(CimTrackerIntentService.REVISION, getAppVersionNumber()); // "revision" = "77"
arrayMap.put("app_key", "barcoo");
arrayMap.put("build_type", "release");
arrayMap.put("uuid", this.settings.getUserUuid());

None of these values is a secret. app_key is just the name of the app flavour. uuid is an install ID that the app generates locally on first start (GlobalId.createCompactId()). The server seems to want the whole set: with no cookies I get 404, with only uuid I get 404, and with the four app cookies but no uuid I get 400. Once all five are there, the request goes through:

curl -s https://www.barcoo.com/product_api/DE/EAN/4036300107756 \
  -A 'barcoo/77' -H 'Accept-Language: de-DE' \
  -b "platform=android; revision=77; app_key=barcoo; build_type=release; uuid=$(uuidgen | tr -d -)" \
  | jq '.product | {pi, title, sub_title, fallback_title, category_name, manufacturer, rating}'
{
  "pi": "4036300107756",
  "title": "Milram Burlander",
  "sub_title": "DMK Deutsches Milchkontor GmbH",
  "fallback_title": "Milram Burlander 150 gr",
  "category_name": "Schnittkäse",
  "manufacturer": "DMK Deutsches Milchkontor",
  "rating": {
    "average": "4,33",
    "raw_average": 4.3333,
    "rounded": 4.5,
    "count": 3
  }
}

The structure is the same as in 2022. There’s also a metadata block and more product fields such as nutrition data and the food traffic light:

"nutritiondata": {
  "nutrition_data": {
    "Brennwert": "1.507kJ",
    "Kalorien": "359,94kcal",
    "Fett": "29g",
    "Kohlenhydrate": "0,1g",
    "Eiweiß": "24g",
    "Zucker": "0,1g"
  },
  "source": "fddb"
},
"lma": {
  "sugar_gram": "0,1", "sugar_color": "green",
  "fat_gram": "29",    "fat_color": "red",
  "sodium_gram": "0,5", "sodium_color": "yellow",
  "salt_gram": "1,25"
},
...
"metadata": {
  "PI": "4036300107756",
  "PINS": "EAN",
  "C": "DE",
  "TITLE": "Milram Burlander",
  "URL": "/EAN/4036300107756/milram-burlander?m=android"
}

Some caveats:

  • The data looks frozen. The response contains "from_short_time_cache": true, and the newest online price for the Milram cheese is from January 2018. That’s fine for naming a product in the fridge, but the prices are useless.
  • The responses have no Access-Control-Allow-Origin header. Browsers also don’t let a page on another origin set cookies for barcoo.com. So this only works from a backend or a script, not from JavaScript on your own website.
  • The server is the same Rails/Passenger application as before (Server: Apache/2.4.41 (Ubuntu), X-Powered-By: Phusion Passenger 5.0.30). barcoo.de now redirects to handelsangebote.de. I wouldn’t build anything long-lived on this.

Offers now come from Offerista’s brochure API#

In parallel, the product screen asks Offerista’s brochure backend which stores currently have offers for that EAN:

// com/shared/rest/OfferService.java
@GET("/offers?type=product&with_store=true&with_company=true")
Single<OfferResult> getProductsByEan(@Query("ean") String str, @Query("geo") String str2);

The base URL comes from the api_base_uri string resource: https://delivery.offerista.com/. The app’s ApiInterceptor adds HTTP Basic authentication to these requests. Key and secret are in the resources (api_key / api_secret) and are read by ApiCredentials. Without them the server answers 401 Unauthorized, with WWW-Authenticate: Basic realm="Webservice Offerista Group GmbH". I didn’t go further than that, because it’s actual authentication and the data on that API belongs to Offerista.

Conclusion#

The trick from 2022, finding the API through the WebView, still leads to the right endpoint four years later. For my fridge scanner it is good enough to name products, but not for prices, and I wouldn’t rely on it long term.

  • Product data by EAN: GET https://www.barcoo.com/product_api/DE/EAN/<ean> still works, as long as you send the app’s cookies (platform, revision, app_key, build_type, uuid). It returns JSON, but the data appears to be an old cache.
  • Search suggestions: GET https://www.barcoo.com/search/suggest?spellcheck.q=<term> still works without anything special.
  • Current offers by EAN: delivery.offerista.com/offers?type=product&ean=<ean>, but only with HTTP Basic authentication.

Sources#

cd ~/writing