HELGE SVERREAll-stack Developer
Bergen, Norwayv13.0
est. 2012  |  300+ repos  |  4000+ contributions
Theme:
Reverse Engineering Norwegian Grocery Apps
February 15, 2026

Why Would Anyone Do This?

In my investigation of Spenderlog, I decompiled a "free spending tracker" and found it was actually a market research data collection tool by DVJ Insights. The app worked by taking your Trumf, Coop, and Rema 1000 credentials and scraping your receipts on your behalf.

That raised an obvious follow-up question: what APIs are these grocery apps actually using? Could you build your own receipt aggregator — one that doesn't ship your purchase history to a Dutch market research agency?

So I pulled the APKs for Rema 1000 and Coop Norway, decompiled them, and mapped every API endpoint. Here's what I found.

Full API documentation, including OpenAPI specs: GitHub Gist

The Three Apps

Norway's grocery market is dominated by three groups:

  • NorgesGruppen (Kiwi, Meny, Spar, Joker) — loyalty program: Trumf
  • Coop (Coop Extra, Coop Prix, Coop Mega, Obs) — app: Coop Medlem
  • Rema 1000 — loyalty app: Æ

Together they cover ~97% of Norwegian grocery retail. Each has an app with purchase history and digital receipts. Each app talks to a backend API that's not publicly documented.

Rema 1000: The Easy One

Package: no.rema.bella | Version: 3.0.12 | Tech: Kotlin, Retrofit2, OkHttp3

Rema's app is a native Android app built with Kotlin. Decompiling it with jadx gives you clean, readable source code with every API endpoint neatly defined as Retrofit interface methods.

How I Decompiled It

# Download the APK
apkeep -a no.rema.bella -d apk-pure ./rema-apk/

# Decompile
jadx -d decompiled/ rema.apk

# The API interfaces are right here:
ls decompiled/sources/no/shortcut/bella/data/remote/api/

The app is developed by Shortcut (a Norwegian digital agency), uses Koin for dependency injection, Jetpack Compose for the UI, and Retrofit2 + OkHttp3 for networking.

Authentication

Rema uses OAuth 2.0 Authorization Code with PKCE, via their own identity provider at id.rema.no:

ParameterValue
Authorizationhttps://id.rema.no/authorization
Tokenhttps://id.rema.no/token
Client IDandroid-251010
Scopeall
PKCEYes (CodeVerifier)

Every API request requires two key headers:

Authorization: Bearer <access_token>
ocp-apim-subscription-key: fb5e24884b504d0bad761098f77e6605

The second header is an Azure API Management subscription key — Rema's backend runs on Azure. The key is hardcoded in the APK.

The Receipt API

The two endpoints that matter:

List all purchases:

GET https://api.rema.no/v1/bella/transaction/v2/heads

Returns every purchase with store name, date, amount, and a transaction ID:

{
  "bonusTotal": 0,
  "purchaseTotal": 4250.80,
  "discountTotal": 312.50,
  "transactions": [
    {
      "purchaseDate": 1600695669000,
      "storeId": "7080",
      "amount": 249.90,
      "storeName": "Rema 1000 Storgate",
      "id": 11223344556,
      "receiptNbr": "2009210000123123123123123"
    }
  ]
}

Get line items for a receipt:

GET https://api.rema.no/v1/bella/transaction/v2/rows/{transactionId}

Returns every item on the receipt with EAN barcode, price, and discounts:

[
  {
    "prodtxt1": "NORVEGIA 1KG",
    "prodtxt3": "7038010009457",
    "productGroupDescription": "Ost",
    "unitPrice": 109.90,
    "amount": 109.90,
    "discount": 20.00
  }
]

The prodtxt3 field is the EAN-13 barcode — a globally unique product identifier. This is exactly what Spenderlog extracts and sends to DVJ Insights.

What Else Is In There

Beyond receipts, the decompiled source reveals the full API surface — 50+ endpoints across transactions, offers, customer profile, shopping lists (with real-time WebSocket sync), Scan & Pay self-checkout, payment cards, Vipps integration, geolocation, product search by GTIN, and even GDPR data access requests (/v1/sardar/DataAccessRequest).

The full list is in the OpenAPI spec.

Security Notes

  • No certificate pinning. No CertificatePinner, no network_security_config.xml. All traffic is interceptable with a standard mitmproxy setup.
  • Tokens are stored in EncryptedSharedPreferences.
  • Mutating operations require a sync token from HEAD /synctoken.

Coop Norway: The Hard One

Package: no.coop.members | Version: 4.17.3 | Tech: Flutter (Dart → ARM32)

Coop's app is built with Flutter. This changes everything about the reverse engineering approach.

Why Flutter Is Different

When you decompile a native Kotlin/Java app with jadx, you get readable source code. When you decompile a Flutter app, you get:

  • Java/Kotlin side: A thin shell — MainActivity, Flutter engine initialization, and platform channel bridges. No business logic.
  • libapp.so: A 19 MB compiled ARM binary containing all the Dart code. Not decompilable to readable Dart.
  • libflutter.so: The Flutter engine itself (8 MB), including BoringSSL for certificate pinning.

You can't read the Dart source code. But you can extract every string literal from the binary.

The strings Approach

# Extract libapp.so from the XAPK split APK
unzip config.armeabi_v7a.apk "lib/armeabi-v7a/libapp.so"

# Extract all URLs
strings libapp.so | grep -E "^https?://" | sort -u

# Extract API paths
strings libapp.so | grep -E "^/user/" | sort -u

# Extract data model names
strings libapp.so | grep -E "^TPurchase" | sort -u

# Extract JSON field names
strings libapp.so | grep -E "^(receiptId|storeName|ean13|amount)" | sort -u

# Extract gRPC service paths
strings libapp.so | grep -E "^/coopnorge\." | sort -u

This works because Dart string literals survive compilation. Method names, class names, JSON serialization keys, URL constants, error messages — they're all in there as plain ASCII.

What I Found

Base URL: https://api.coop.no

Authentication: OpenID Connect via Auth0 at https://login.coop.no/. I confirmed this by fetching the well-known configuration:

curl -s https://login.coop.no/.well-known/openid-configuration | jq .issuer
# "https://login.coop.no/"

Auth0-hosted OIDC with PKCE (S256), supporting scopes like openid, profile, email, phone, offline_access.

Payment auth (Coopay): Aera SDK from Giant Leap at https://api.aerahost.com/ — handles BankID verification and payment signing via biometrics.

The Receipt Endpoints

GET /user/pay/history/dashboard     → Spending overview
GET /user/pay/history/list          → Purchase list
GET /user/pay/history/month         → Monthly breakdown
GET /user/pay/history/details       → Full receipt with line items
GET /user/pay/history/search        → Search by product/store
GET /user/pay/history/receipt.pdf   → Download receipt as PDF

The data model, reconstructed from Dart class names and JSON field names:

PurchaseSummary {
  summaryId, receiptId, purchaseDate,
  storeName, storeId, chainId,
  amount, totalDiscount, memberBonus,
  lines: PurchaseSummaryLine[]
}

PurchaseSummaryLine {
  productName, ean13, gtin13, barcode,
  quantity, amount, discount, unitPrice
}

Beyond Receipts

The strings extraction revealed 70+ API endpoints covering:

  • Coupons: /coupon/all, /coupon/activate, /coupon/swap
  • Coopay (mobile payment): /user/pay/activate, /user/pay/devices, /user/pay/scancodes
  • Shop Express (scan & go): /user/shopexpress/shoppingtrip/init, /user/shopexpress/shoppingtrip/add_cart_item
  • Family: /user/family/myfamily, /user/family/send_invitation
  • Parking: /user/parking/history, /user/vehicle/list
  • Mastercard: /user/mastercard/card-info, /user/mastercard/movements
  • BankAxept: /user/bankaxept/enrollment
  • gRPC Shopping Lists at handleliste.coop.no (protobuf over HTTP/2)

Full list in the OpenAPI spec.

The XAPK Problem

One complication: the Coop APK from APKPure only ships with armeabi-v7a (32-bit ARM) native libraries. Modern Android emulators on Apple Silicon are 64-bit only and dropped 32-bit support. This means you can't easily run the app in an emulator for dynamic analysis.

Options for live traffic interception:

  1. Physical Android phone — plug in via USB, run Frida. Works immediately.
  2. Older Android emulator image (Android 12 or below) — still supports 32-bit.
  3. Repack the APK — inject libapp.so into the base APK, remove isSplitRequired, re-sign.
  4. Skip it entirely — the strings approach gives you 95% of what you need without running the app.

Trumf / NorgesGruppen

Trumf is already documented by the community. ttyridal/trumf-data-fetch is a working Python script that:

  1. Authenticates with phone + password
  2. Fetches transactions from https://platform-rest-prod.ngdata.no/trumf/husstand/transaksjoner
  3. Fetches line items from /trumf/husstand/transaksjoner/detaljer/{batchid}

Each transaction includes Norwegian field names (dato, beskrivelse, belop, trumf) and line items with vareTekst, ean, antall, belop.

Building Your Own Receipt Fetcher

With all three APIs mapped, you could build a self-hosted receipt aggregator. Here's the approach:

For Rema 1000

The API is the most accessible. No certificate pinning, clean REST endpoints, well-documented from decompilation.

  1. Implement OAuth 2.0 PKCE flow against id.rema.no
  2. Include the subscription key header on every request
  3. Call /v1/bella/transaction/v2/heads to list purchases
  4. Call /v1/bella/transaction/v2/rows/{id} for line items
  5. Store product data using prodtxt3 (EAN) as the key

An existing Node.js wrapper already exists: Starefossen/node-rema-ae-api.

For Coop Norway

More work needed — the exact request/response format hasn't been verified via live interception. But the endpoints and data model are mapped:

  1. Implement OIDC flow against login.coop.no (standard Auth0)
  2. Call /user/pay/history/list for purchase stubs
  3. Call /user/pay/history/details for full receipts with line items
  4. Or download PDFs via /user/pay/history/receipt.pdf?receiptid=

For Trumf

Use trumf-data-fetch directly — it's a working implementation.

The Bigger Picture

What Spenderlog does — collecting receipt data from multiple grocery chains and aggregating it — is technically straightforward. The APIs exist. The data is structured. The authentication is standard OAuth/OIDC.

The question isn't whether you can build this. It's whether you should, and who benefits when you do.

When you build it for yourself, you get a spending tracker that keeps your data on your own infrastructure. When DVJ Insights builds it and advertises it as "100% free," Norwegian households get a spending chart and a market research agency gets product-level purchase data from 4,000-5,000 households to sell to FMCG brands.

Same APIs. Same data. Very different business models.


Resources:

Found this useful? Have corrections or additions? Reach out on Twitter/X.




<!-- generated with nested tables and zero regrets -->