Skip to content

About

Ruby client for the AppBlockList App Lookup API: filtering categories, age rating and iOS/Android IDs for 1.87M apps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

appblocklist (Ruby)

Turn an app identifier into a policy decision. This gem asks the AppBlockList App Lookup API what an iOS or Android app is, and hands you a plain Ruby object you can test, store and act on.

AppBlockList.lookup("com.zhiliaoapp.musically").filtering_categories
# => ["Social Networking", "Streaming Media"]

The gem uses only net/http and json from the standard library and supports Ruby 2.7 and newer. It fits into a Rails app, a Sidekiq worker, a Hanami service or a cron script on the MDM jump box.

Install

With Bundler:

# Gemfile
gem "appblocklist", "~> 1.0"
bundle install

Or on its own:

gem install appblocklist

Then provide your key. The client looks for APPBLOCKLIST_API_KEY in the environment, which plays well with Rails credentials loaded into ENV, Kamal secrets, Heroku config vars and systemd EnvironmentFile= setups.

export APPBLOCKLIST_API_KEY=your_key_here

Keys come with an App Lookup API plan from the AppBlockList pricing page.


A short tour in irb

require "appblocklist"

client = AppBlockList::Client.new            # or Client.new(api_key: "...")

tiktok = client.lookup("com.zhiliaoapp.musically")
tiktok.found?               # => true
tiktok.name                 # => "TikTok - Videos, Shop & LIVE"
tiktok.developer            # => "TikTok Ltd."
tiktok.age_rating           # => "12+"
tiktok.minimum_age          # => 12
tiktok.app_store_id         # => "835599320"
tiktok.android_package      # => "com.zhiliaoapp.musically"
tiktok.filtering_categories # => ["Social Networking", "Streaming Media"]
tiktok.primary_category     # => "Social Networking"
tiktok.iab_category         # => "Technology & Computing > Computing > Internet > Social Networking"

client.lookup("id835599320").bundle_id   # App Store ID with prefix
# => "com.zhiliaoapp.musically"

ghost = client.lookup("com.example.not.a.real.app")
ghost.found?                # => false
ghost.error                 # => "App not found."

Three ID shapes are accepted, up to 200 characters:

  • iOS bundle ID, as listed in Jamf, Intune, Kandji or Apple Business Manager inventories
  • App Store ID, the digits in an apps.apple.com URL, with or without the id prefix
  • Android package name, as listed on Google Play and in Android Enterprise consoles

The key travels in the X-API-Key header. Every request carries User-Agent: appblocklist-ruby/1.0.0 (+https://www.appblocklist.com).


The AppResult object

AppBlockList::AppResult is immutable. String fields are never nil: a missing value is "", and filtering_categories is always an array.

Reader Type Notes
found? Boolean false for a 404 answer
name, developer String store listing values
bundle_id String iOS bundle ID
app_store_id String digits, kept as a string
android_package String Google Play twin, "" if the app is on one store only
app_store_genre String one of 26 App Store genres
age_rating String 4+, 9+, 12+, 17+
filtering_categories Array of String ranked, most descriptive first
developer_domain String e.g. tiktok.com
iab_category String IAB content taxonomy path
error String or nil the service message on a not-found answer
raw Hash frozen copy of the decoded JSON

Policy helpers:

BLOCKED = ["Gaming", "Gambling", "Dating", "Adult", "VPN / Proxy / Anonymizers"].freeze

app.blocked_by?(BLOCKED)         # => true / false
app.matching_categories(BLOCKED) # => ["Gaming"]  (for the audit log)
app.category?("gaming")          # case-insensitive
app.pending_android?             # Google Play app not yet categorized
app.to_h                         # string keys, same names as the API
app.to_json
AppBlockList::AppResult.from_hash(hash) # rebuild from a cached Hash

The 58 filtering categories are the same ones used to categorize 120 million websites. A single BLOCKED constant can therefore drive both your app policy and your DNS or proxy policy.


API surface

Call HTTP requests Returns or raises
AppBlockList.lookup(id) 1 shortcut on a shared client built from ENV
Client.new(**opts) 0 raises AuthenticationError if no key is found
Client#lookup(id) 1, plus retries on 5xx AppResult
Client#lookup_each(ids, pause: 0) { |id, result| } 1 per unique ID Hash of id => AppResult or nil
Client.parse_response(status, body) 0 AppResult or raises; useful in tests
Client.normalize_id(id) 0 stripped ID or raises InvalidIdError

Constructor options:

Option Default Purpose
api_key: ENV["APPBLOCKLIST_API_KEY"] your key
timeout: 15 open and read timeout, seconds
retries: 2 extra attempts after 5xx or network errors
retry_delay: 1.0 first back-off in seconds, doubled each time
base_url: https://www.appblocklist.com/api/v1/app override for an internal proxy
user_agent: appblocklist-ruby/1.0.0 (+https://www.appblocklist.com)
transport: Net::HTTP ->(uri, headers, timeout) { [status, body] }

lookup_each sends requests strictly in sequence. That is what the service asks for: one ID per request, one request after another, no parallel bursts. Do not wrap the client in a thread pool to speed up a sync; spread a big job over time instead.


Errors, as a decision table

Situation Status Raised Typical response in a job
App found 200 nothing use the result
Unknown app 404 nothing, found? == false mark for manual review
Google Play only, not yet categorized 404 nothing, pending_android? == true review, retry next month
Bad ID format 400 InvalidIdError log the row, continue
Missing or wrong key 401 AuthenticationError stop, alert ops
Monthly lookups used 403 QuotaExceededError (#monthly_lookups) stop, reschedule
Temporary failure 500 ServerError after retries let the job framework retry
Network, TLS, timeout none TransportError after retries let the job framework retry

Every class inherits from AppBlockList::Error, which exposes #status and #body.

begin
  app = client.lookup(params[:app_id])
rescue AppBlockList::InvalidIdError
  render json: { error: "Not a bundle ID, App Store ID or package name" }, status: :unprocessable_entity
rescue AppBlockList::QuotaExceededError => e
  Rails.logger.warn("AppBlockList quota: #{e.monthly_lookups}")
  render json: { decision: "review" }
end

Recipe 1: an ActiveJob that keeps an approved-apps table honest

Schools and companies often keep a table of apps that were approved at some point. Apps change: a note-taking app adds an AI chat, a game adds real-money spins. This job re-checks each approved app and demotes the ones that drifted into a blocked category.

# app/models/approved_app.rb
# columns: bundle_id:string, status:string, reason:string,
#          categories:json, age_rating:string, checked_at:datetime
class ApprovedApp < ApplicationRecord
  BLOCKED = ["Gaming", "Gambling", "Dating", "Adult", "AI / LLM Tools", "VPN / Proxy / Anonymizers"].freeze
end
# app/jobs/recheck_approved_apps_job.rb
class RecheckApprovedAppsJob < ApplicationJob
  queue_as :low

  # Sidekiq and GoodJob both honour these.
  retry_on AppBlockList::ServerError, AppBlockList::TransportError, wait: 10.minutes, attempts: 5
  discard_on AppBlockList::AuthenticationError

  def perform(batch_size: 500)
    client = AppBlockList::Client.new(retries: 1)

    ApprovedApp.where("checked_at IS NULL OR checked_at < ?", 30.days.ago)
               .order(:checked_at).limit(batch_size).each do |record|
      app = client.lookup(record.bundle_id)
      record.update!(attributes_for(app))
      sleep 0.1 # be polite on a shared key
    rescue AppBlockList::InvalidIdError
      record.update!(status: "invalid", checked_at: Time.current)
    end
  rescue AppBlockList::QuotaExceededError
    self.class.set(wait_until: Time.current.next_month.beginning_of_month).perform_later
  end

  private

  def attributes_for(app)
    base = { checked_at: Time.current }
    return base.merge(status: "review", reason: app.error) unless app.found?

    hits = app.matching_categories(ApprovedApp::BLOCKED)
    base.merge(
      status: hits.empty? ? "approved" : "blocked",
      reason: hits.join(", ").presence,
      categories: app.filtering_categories,
      age_rating: app.age_rating
    )
  end
end

Schedule it daily with whatever you already use (Solid Queue recurring tasks, sidekiq-cron, GoodJob cron). Because only apps older than 30 days are re-checked and the batch is capped, the lookup spend stays flat as the table grows.


Recipe 2: a rake task that writes the MDM blocklist files

Once statuses are current, MDM tools need plain lists. iOS needs bundle IDs; Android Enterprise needs package names. This task writes both, using the android_package twin stored from an earlier lookup or fetched now.

# lib/tasks/mdm.rake
namespace :mdm do
  desc "Write tmp/mdm/ios_blocked.txt and tmp/mdm/android_blocked.txt"
  task export: :environment do
    out = Rails.root.join("tmp/mdm")
    FileUtils.mkdir_p(out)
    client = AppBlockList::Client.new

    ios = ApprovedApp.where(status: "blocked").pluck(:bundle_id).sort
    android = []

    client.lookup_each(ios) do |_id, app|
      android << app.android_package if app&.found? && !app.android_package.empty?
    end

    File.write(out.join("ios_blocked.txt"), ios.join("\n") + "\n")
    File.write(out.join("android_blocked.txt"), android.uniq.sort.join("\n") + "\n")
    puts "iOS: #{ios.size}  Android: #{android.uniq.size}"
  end
end

Run it from cron after the job above:

30 3 * * * cd /srv/school-mdm && RAILS_ENV=production bin/rails mdm:export >> log/mdm_export.log 2>&1

How those files reach devices depends on the console. For iOS the list ends up in the Restrictions payload of a configuration profile, documented in Apple's Device Management reference. For Android, managed Google Play and the policies described in Android Enterprise for developers control what can be installed. AppBlockList has console-specific walkthroughs for blocking apps in Intune, Jamf Pro and the Google Admin console.

The repository ships a fuller variant as examples/sync_approved_apps.rake.


Recipe 3: a helpdesk command line tool

Support staff get questions like "is this app allowed on student phones?" A ten-line executable answers them without opening the MDM console.

#!/usr/bin/env ruby
# bin/appcheck  com.discord 985746746 com.roblox.client
require "appblocklist"

BLOCKED = %w[Gaming Gambling Dating Adult].push("VPN / Proxy / Anonymizers").freeze

AppBlockList::Client.new.lookup_each(ARGV) do |id, app|
  verdict =
    if app.nil? then "invalid id"
    elsif !app.found? then "unknown, ask IT"
    elsif app.blocked_by?(BLOCKED) then "BLOCKED (#{app.matching_categories(BLOCKED).join(', ')})"
    else "allowed"
    end
  puts "#{id.ljust(28)} #{app&.name.to_s.ljust(36)} #{app&.age_rating.to_s.ljust(4)} #{verdict}"
end
$ bin/appcheck com.roblox.robloxmobile com.duolingo.DuolingoMobile
com.roblox.robloxmobile      Roblox                               12+  BLOCKED (Gaming)
com.duolingo.DuolingoMobile  Duolingo: Learn Languages            4+   allowed

Names, ratings and categories come from the API at run time, so the output follows the store listing as it changes.


Why a category API beats a static list

  • Lists rot, categories do not. A new VPN app published tomorrow lands in VPN / Proxy / Anonymizers without anyone editing your code.
  • Both stores at once. About 800,000 iOS records carry their Google Play twin, and 3.9 million Google Play package names are tracked, so a mixed iPad and Chromebook or Android fleet uses one policy.
  • The same vocabulary as your web filter. 58 categories shared with the website database mean the board approves one list, not two.
  • Auditable. Store matching_categories with each decision and you can answer "why was this blocked?" months later.

Security frameworks point the same way. NIST SP 800-124 Rev. 2 on managing mobile devices in the enterprise lists app vetting and allow or deny lists among the core controls, and the mobile device management article on Wikipedia gives a vendor-neutral overview of how those controls are pushed to devices.


What it costs

Offer Price
App Lookup API $99 per month for 100,000 lookups, higher volumes quoted
App Blocklists for MDM (School or Business) $99/month or $990/year per organization
Districts and IT Providers $249/month or $2,490/year, unlimited schools or clients, includes the API with 100,000 lookups a month
Mobile App Database top 100,000 apps $999, top 500,000 apps $1,999, one-time

Only 200 answers count as lookups. If you want the lists without writing a sync job, the MDM blocklist feed is refreshed every month in six formats: iOS bundle ID list, Android package list, .mobileconfig, Intune, Jamf and full CSV.


FAQ

Is the client thread-safe?

Each lookup opens its own Net::HTTP connection and the client holds no mutable state after construction, so sharing one instance across Puma threads is fine. Keep the total request rate modest and sequential per job, as the service asks.

Why does a 404 not raise?

Because unknown apps are ordinary in an inventory: in-house apps, TestFlight builds and regional apps. Raising would force a rescue around every row. You get found? == false and decide.

How do I test my own code without hitting the API?

Pass a transport: lambda, or call Client.parse_response(200, json_string) to build results from fixtures:

client = AppBlockList::Client.new(api_key: "x", transport: ->(*) { [200, File.read("test/fixtures/tiktok.json")] })

Can I send many IDs in one call?

No, one ID per request. For bulk analysis of the whole App Store, license the mobile app database: 1,874,397 iPhone and iPad apps in 12 columns.

Where do I get ready-made lists for AI apps or VPNs?

AppBlockList publishes an AI apps list and a VPN and proxy apps list. They are a good seed for an approved or blocked table.

How do I look up a bundle ID by hand?

Use the bundle ID lookup tool for iOS or the Android package name lookup.


Running the tests

bundle install
bundle exec rake test

The Minitest suite parses sample JSON and uses a fake transport. It never opens a socket.


Links

Sister services by Alpha Quantum:


Related packages


License

Released under the MIT License. Copyright (c) 2026 Alpha Quantum.

About

Ruby client for the AppBlockList App Lookup API: filtering categories, age rating and iOS/Android IDs for 1.87M apps

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages