Skip to main content

appblocklist for Python

appblocklist is the Python client for the AppBlockList app categorization API. Give it an iPhone bundle ID, an App Store ID or a Google Play package name and it returns a typed AppResult: name, developer, both store identifiers, genre, age rating, IAB category and up to several ranked filtering categories out of 58.

It is written for the scripts that MDM and IT teams actually run: inventory audits, nightly policy jobs, approval forms and profile generators. The only dependency is requests.

Contents

  1. Install
  2. Five-minute tour
  3. The AppResult object
  4. Client reference
  5. Errors and retries
  6. Recipes
  7. Why use it
  8. FAQ
  9. Links and related packages

1. Install

pip install appblocklist

Python 3.7 or newer. In a project with a virtual environment:

python3 -m venv .venv
. .venv/bin/activate
pip install appblocklist

Pin it in requirements.txt as appblocklist==1.0.0 if you deploy jobs to several machines.

2. Five-minute tour

from appblocklist import AppBlockList

client = AppBlockList("YOUR_API_KEY")

app = client.lookup("com.openai.chatgpt")
print(app.found)                  # True
print(app.name, "/", app.developer)
print(app.filtering_categories)   # ranked list, most descriptive first
print(app.primary_category)
print(app.age_rating)             # e.g. '12+'
print(app.bundle_id, app.android_package)

Leave the key out of the code and set it once in the shell, a systemd unit or your CI secrets:

export APPBLOCKLIST_API_KEY=your_key
client = AppBlockList()  # picks up APPBLOCKLIST_API_KEY

The same app can be reached through any of its identifiers. These three calls return the same record:

client.lookup("com.zhiliaoapp.musically")  # iOS bundle ID
client.lookup("835599320")                 # App Store ID
client.lookup("id835599320")               # App Store ID with prefix

Asking a yes or no question is one line:

if client.is_blocked("com.zhiliaoapp.musically", ["Social Networking", "Gambling"]):
    print("blocked by policy")

3. The AppResult object

AppResult is a dataclass, so it prints readably, compares by value and converts with dataclasses.asdict().

Attribute Type Example Notes
query str 'id835599320' what you passed in, trimmed
found bool True False for an app that is not in the categorized database
name str 'TikTok - Videos, Shop & LIVE' App Store name
developer str 'TikTok Ltd.' publisher
bundle_id str 'com.zhiliaoapp.musically' iOS identifier
app_store_id str '835599320' kept as a string, as the API sends it
android_package str 'com.zhiliaoapp.musically' Google Play twin, empty when there is none
app_store_genre str 'Entertainment' one of 26 genres
age_rating str '12+' 4+, 9+, 12+ or 17+
filtering_categories list[str] ['Social Networking', 'Streaming Media'] ranked
developer_domain str 'tiktok.com' the publisher's website
iab_category str 'Technology & Computing > ...' IAB content taxonomy path
error str or None 'App not found.' API message for a not-found app
raw dict the untouched JSON body

Two helpers live on the object:

  • primary_category: the first filtering category, or None.
  • in_category(names): case-insensitive test against a string or any iterable of names.

4. Client reference

AppBlockList(
    api_key=None,          # falls back to APPBLOCKLIST_API_KEY
    timeout=15.0,          # seconds per request
    retries=2,             # extra attempts on HTTP 500 and network errors
    retry_delay=1.0,       # linear backoff base, seconds
    send_key_as="header",  # or "query" to send api_key= in the URL
    base_url="https://www.appblocklist.com/api/v1/app",
    session=None,          # bring your own requests.Session (proxies, adapters)
)
Method Returns Lookups counted
lookup(app_id) AppResult 1 when found
is_blocked(app_id, categories) bool 1 when found
categories(app_id) list[str] 1 when found
lookup_many(app_ids, pause=0.1) list in input order 1 per unique found app

Under the hood every call is a single GET https://www.appblocklist.com/api/v1/app?id=... with the key in the X-API-Key header and the User-Agent appblocklist-python/1.0.0 (+https://www.appblocklist.com).

lookup_many is deliberately sequential. The service asks for one ID per request and no parallel bursts, so the method sleeps pause seconds between calls and looks up repeated IDs only once.

5. Errors and retries

AppBlockListError
├── BadRequestError       400
├── AuthenticationError   401
├── QuotaExceededError    403  (.monthly_lookups)
└── ServerError           500 and network failures
Status Outcome in Python Meaning
200 AppResult(found=True) categorized app, one lookup counted
404 AppResult(found=False) App not found. or Listed on Google Play, not yet in the categorized database.
400 BadRequestError id missing or not a recognizable identifier
401 AuthenticationError missing or invalid key
403 QuotaExceededError monthly lookups used; exc.monthly_lookups has the allowance
500 ServerError after retries temporary, safe to retry

A 404 is returned rather than raised because "this app is unknown" is normal data in an inventory loop. Every exception carries .status, .message and the parsed .body.

In lookup_many, a 400 or 500 for one ID becomes a dict {"query", "found": False, "error", "status"} in the output list. A 401 or 403 is raised immediately, because every later call would fail the same way.

from appblocklist import AppBlockList, QuotaExceededError, AppResult

client = AppBlockList()
try:
    results = client.lookup_many(ids)
except QuotaExceededError as exc:
    raise SystemExit("Out of lookups for this month: {}".format(exc.monthly_lookups))

failed = [r for r in results if not isinstance(r, AppResult)]

6. Recipes

Recipe A: generate a .mobileconfig that blocks apps by category

Apple's Restrictions payload (com.apple.applicationaccess) takes a blockedAppBundleIDs array, as described in Apple's Device Management documentation. This script turns a candidate list into a signed-ready profile using only the standard library plus this package.

import plistlib
import sys
import uuid

from appblocklist import AppBlockList, AppResult

BLOCK = {"Gambling", "Dating", "Adult", "VPN / Proxy / Anonymizers", "Social Networking"}

candidates = [line.strip() for line in open(sys.argv[1]) if line.strip()]
client = AppBlockList()
blocked = sorted({
    r.bundle_id
    for r in client.lookup_many(candidates, pause=0.15)
    if isinstance(r, AppResult) and r.found and r.bundle_id and set(r.filtering_categories) & BLOCK
})

payload = {
    "PayloadType": "com.apple.applicationaccess",
    "PayloadVersion": 1,
    "PayloadIdentifier": "org.example.appblocklist.restrictions",
    "PayloadUUID": str(uuid.uuid4()).upper(),
    "PayloadDisplayName": "Blocked apps",
    "blockedAppBundleIDs": blocked,
}
profile = {
    "PayloadType": "Configuration",
    "PayloadVersion": 1,
    "PayloadIdentifier": "org.example.appblocklist",
    "PayloadUUID": str(uuid.uuid4()).upper(),
    "PayloadDisplayName": "AppBlockList restrictions",
    "PayloadContent": [payload],
}
with open("blocked-apps.mobileconfig", "wb") as fh:
    plistlib.dump(profile, fh)
print("{} bundle IDs written".format(len(blocked)))

If you would rather not maintain this yourself, the ready-made mobileconfig guide explains the monthly profile that ships with the blocklist feed.

Recipe B: per-device audit of an MDM export

The repository ships examples/audit_inventory.py. It reads a CSV with device and app_id columns, looks up each distinct app once and writes audit.csv listing every device that has an app in a blocked category or rated above 12+.

python examples/audit_inventory.py inventory.csv
# 37 violations written to audit.csv

The key trick is grouping by app before calling the API, so a fleet of 2,000 iPads with the same 300 apps costs 300 lookups, not 600,000.

Recipe C: app approval form with Flask

from flask import Flask, jsonify, request
from appblocklist import AppBlockList, BadRequestError

app = Flask(__name__)
client = AppBlockList()

HARD_NO = {"Gambling", "Adult", "Dating", "Illegal Drugs", "Weapons"}
ASK_A_TEACHER = {"Social Networking", "Gaming", "Streaming Media", "AI / LLM Tools"}

@app.post("/approve")
def approve():
    try:
        r = client.lookup(request.form.get("app_id", ""))
    except BadRequestError as exc:
        return jsonify(error=exc.message), 400
    if not r.found:
        return jsonify(decision="manual", reason=r.error)
    cats = set(r.filtering_categories)
    if cats & HARD_NO or r.age_rating == "17+":
        decision = "deny"
    elif cats & ASK_A_TEACHER:
        decision = "manual"
    else:
        decision = "approve"
    return jsonify(decision=decision, app=r.name, categories=r.filtering_categories, age=r.age_rating)

Schools receiving E-rate funds must filter content that is harmful to minors under the Children's Internet Protection Act. An approval form that records the category and age rating for each decision gives you a paper trail for that policy.

Recipe D: weekly cron job that reports new risky apps

# /opt/appaudit/weekly.py
import json
import pathlib
from appblocklist import AppBlockList, AppResult

STATE = pathlib.Path("/var/lib/appaudit/seen.json")
WATCH = {"VPN / Proxy / Anonymizers", "Remote Access Tools", "Cryptocurrency", "AI / LLM Tools"}

seen = set(json.loads(STATE.read_text())) if STATE.exists() else set()
current = set(pathlib.Path("/var/lib/appaudit/inventory.txt").read_text().split())
new_ids = sorted(current - seen)

client = AppBlockList()
for r in client.lookup_many(new_ids):
    if isinstance(r, AppResult) and r.found and set(r.filtering_categories) & WATCH:
        print("{:<45} {:<30} {}".format(r.query, r.name[:30], ", ".join(r.filtering_categories)))

STATE.write_text(json.dumps(sorted(current)))
0 6 * * 1  APPBLOCKLIST_API_KEY=your_key /opt/appaudit/.venv/bin/python /opt/appaudit/weekly.py | mail -s "New apps to review" it@example.org

Only IDs that appeared since last week are looked up, which keeps the monthly count low.

7. Why use it

  • One record per app across both stores. About 800,000 apps carry their Android twin, so an iOS-only inventory still produces an Android package list.
  • 58 filtering categories that match the web side. The taxonomy is the one applied to 120 million web domains, which means one policy can cover a site and its app.
  • Age rating alongside categories. Policies for younger students often hinge on 17+ regardless of category.
  • Free misses. Only a 200 answer is counted. Unknown apps, bad IDs and server errors cost nothing.
  • Typed and testable. A dataclass result, a small exception tree and an injectable requests.Session make it easy to mock in your own tests.

For the full catalogue as a file, the Mobile App Database covers 1,874,397 iPhone and iPad apps in 12 columns. Teams that just want the finished lists can take the monthly blocklist feed for MDM in six formats.

Plan Price
App Lookup API $99 per month, 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 client organizations, API with 100,000 lookups a month included
Mobile App Database $999 one-time (top 100,000 apps), $1,999 one-time (top 500,000 apps)

See the AppBlockList pricing page for current terms.

8. FAQ

How do I mock the client in my own unit tests?

Pass a mock.Mock(spec=requests.Session) as session= and set session.get.return_value to an object with status_code and json(). The package's own tests/test_client.py shows the pattern with no network access.

Is it safe to run lookups in threads?

The API asks for requests one after another, so avoid thread pools. lookup_many already paces requests, and grouping by unique ID usually cuts the number of calls far more than parallelism would.

What happens when an app is only on Google Play?

If the package name is known but not yet categorized, you get found=False, an error saying it is listed on Google Play, and android_package filled in. Treat it as a manual review item.

Can I look up an app by its name?

No. The API takes identifiers only. Use the free bundle ID finder to turn a name into a bundle ID first.

Does categorizing apps help with children's privacy rules?

Category and age rating are inputs to a policy, not legal advice. For services directed at children under 13 in the US, the FTC's COPPA Rule is the reference text.

Which Python versions are tested?

The package targets Python 3.7 and newer and uses only requests plus the standard library.

What is MDM, briefly?

Mobile device management is the practice of enrolling phones and tablets in a central console that pushes settings, apps and restrictions. The Wikipedia article on mobile device management is a good neutral overview.

Same API, other runtimes:

Other Alpha Quantum data products for the network side:

License

MIT, Copyright (c) 2026 Alpha Quantum.

Metadata

Release files for appblocklist 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for appblocklist 1.0.0
File Size Uploaded Provenance Actions
appblocklist-1.0.0.tar.gz 22.3 kB Without provenance Details for appblocklist-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for appblocklist 1.0.0
File Interpreter ABI Platform Actions
appblocklist-1.0.0-py3-none-any.whl Python 3 none any Details for appblocklist-1.0.0-py3-none-any.whl

Total release size: 34.4 kB

Release files / appblocklist-1.0.0.tar.gz

Download URL appblocklist-1.0.0.tar.gz
Size 22.3 kB
Tags Source
SHA-256 checksum
How to use checksums
1279a2e1d6a63620883360605dd9cd6efd1b8105cda887a1afa93cf37270c402
BLAKE2b-256 checksum
How to use checksums
e01062ece6fbe33a8a259d7f46609707969242a9ea5ba388217c684af59dfc5a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.8.10

Release files / appblocklist-1.0.0-py3-none-any.whl

Download URL appblocklist-1.0.0-py3-none-any.whl
Size 12.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5832229b88fcd42959162b05315d7bdd6112663bfaf4ec29947d51795b90f3da
BLAKE2b-256 checksum
How to use checksums
3b57149b9b6e83971d718c5b609d3495361570cb7307a37f3984857a06668eb8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.8.10

Security for appblocklist 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page