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
- Install
- Five-minute tour
- The
AppResultobject - Client reference
- Errors and retries
- Recipes
- Why use it
- FAQ
- 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, orNone.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.Sessionmake 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.
9. Links and related packages
Links
- Product: https://www.appblocklist.com/
- Lookup API page: https://www.appblocklist.com/app-categorization-api.php
- Google Admin guide: https://www.appblocklist.com/mdm/google-admin-block-apps.php
- VPN and proxy apps list: https://www.appblocklist.com/lists/vpn-proxy-apps-list.php
- Package name lookup: https://www.appblocklist.com/tools/package-name-lookup.php
- GitHub: https://github.com/explainableaixai/appblocklist
- GitLab: https://gitlab.com/url-classifications/appblocklist
- Support: https://www.appblocklist.com/contact.php, info@alpha-quantum.com
Related packages
Same API, other runtimes:
- appblocklist on npm for Node.js
- appblocklist-go
- appblocklist-rust
- appblocklist-java
- appblocklist-dotnet
- appblocklist-php
- appblocklist-ruby
- appblocklist-dart
- appblocklist-elixir
Other Alpha Quantum data products for the network side:
- The web filtering database of 120 million domains in the same 58 categories
- aitoolsblocklist on PyPI and the AI blocklist for web filtering
- CIPA compliant web filter for schools and libraries
- Domain categorization database
- Website classification API
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.