A local web GUI (plus a small CLI) for the Apple Business Manager and Apple School Manager APIs, built for Mac admins. Browse your device inventory, MDM server assignments, AppleCare coverage, users, groups, apps, blueprints, and audit events — organized by category, with dashboards and one-click CSV exports.
- KISS: one Python package, server-rendered HTML, no database, no JS framework, runs entirely on your Mac.
- Open source, internal-IT-shaped: not a SaaS.
abapit servestarts a local web app bound to127.0.0.1and opens your browser. - Plays nice with scripts:
abapit export devices -o devices.csvandabapit token(prints a bearer token forcurl) fit munki/autopkg-style automation.
⚠️ Use at your own risk. abapit is a community tool, not affiliated with or endorsed by Apple. Most of it is read-only, but the assignment feature modifies your organization (device↔MDM moves affect enrollment). Every write is gated behind a dry-run preview and explicit confirmation — review previews carefully. Provided as-is, without warranty (MIT).
Needs Python 3.10+ (brew install python if your Mac doesn't have it).
git clone https://github.com/goetchstone/abapit && cd abapit
python3 -m venv .venv && .venv/bin/pip install -e .
.venv/bin/abapit serve --demo # full UI with a fake fleet — kick the tiresOr as a one-liner without cloning, if you have pipx:
pipx install git+https://github.com/goetchstone/abapit
It's a web app under the hood, but nobody should have to babysit a terminal:
abapit install-appThat registers a login service (abapit runs in the background on
127.0.0.1:8866, restarts if it crashes, logs to ~/Library/Logs/abapit.log)
and creates ~/Applications/abapit.app — click it like any Mac app, drag
it to your Dock. For a standalone window with its own icon, open
http://127.0.0.1:8866 in Safari and choose File → Add to Dock.
After pulling new code, run abapit restart-app so the service loads it.
abapit uninstall-app removes both.
Every Apple API call is logged with status and latency, along with token mints, rate-limit backoffs, and background-refresh failures:
2026-06-12 10:23:34 INFO GET https://api-business.apple.com/v1/orgDevices -> 200 (861 ms)
With the login service, logs land in ~/Library/Logs/abapit.log; in a
terminal they go to stderr. Set ABAPIT_LOG=debug (or warning) to change
verbosity. If you ever wonder whether abapit is hammering Apple:
grep minting ~/Library/Logs/abapit.log — you should see at most one per
org per hour.
- In Apple Business Manager (or Apple School Manager), sign in as an Administrator and go to your account name → Preferences → API.
- Create an API account and download the private key (a
.pemfile — you can only download it once). Note the Client ID and Key ID. - Run
abapit serve, open Settings, and add the org: paste the key (or point at the file), enter the Client ID and Key ID, pick Business or School, then hit Test.
Multiple orgs are supported — add another credential set and switch from the
dropdown in the header. Config lives in ~/.config/abapit/config.json;
private keys are stored separately in ~/.config/abapit/keys/ with 0600
permissions.
abapit implements Apple's OAuth client-credentials flow: it signs an ES256
JWT client assertion with your private key (sub = client ID, aud =
https://account.apple.com/auth/oauth2/v2/token), exchanges it at
https://account.apple.com/auth/oauth2/token for a one-hour bearer token
(scope business.api or school.api), and refreshes automatically on expiry
or 401. Nothing is sent anywhere except account.apple.com and
api-business.apple.com / api-school.apple.com.
| Category | Contents |
|---|---|
| Dashboard | Fleet counts, devices added per month, product family/status breakdowns, devices per MDM server, devices not assigned to any MDM, recent audit events |
| Devices | Searchable inventory, per-device detail incl. AppleCare/warranty coverage and assigned MDM server |
| MDM Servers | Device management services with assigned-device lists |
| Assign to MDM | The one write: move devices between MDM services — paste serials (or prefill all unassigned), dry-run preview of exactly what changes, explicit confirm, then live tracking of Apple's batch activity |
| Apple MDM Enrolled | Devices enrolled in Apple's built-in MDM |
| Users & User Groups | Managed Apple Accounts, group membership (Business only) |
| Apps & Packages | VPP/custom apps and packages (Business only) |
| Blueprints & Configurations | Blueprints with their attached apps/packages/configs (Business only) |
| Audit Events | Org audit log with date-range and type filters (Business only) |
| Changes | Snapshot-to-snapshot diffs: devices added/removed, MDM assignment moves, field-level attribute changes |
| Coverage | AppleCare/warranty expiry report from the latest snapshot — "what expires in 30/60/90/180/365 days" plus devices with no active coverage; instant at any fleet size |
| Fleet Age | Refresh planning: age distribution from order dates, devices older than N years, cross-referenced with coverage ("old and uncovered — replace first"), CSV for the budget meeting |
| CSV everywhere | /export/devices.csv, applecare.csv, users, apps, … and the same via abapit export |
Apple School Manager orgs see the device-related sections (that's what Apple's School API exposes today).
Apple's API only shows the current state of your org. Snapshots give you
history: each one stores a full point-in-time copy of your org in a single
SQLite database (~/.local/share/abapit/history.sqlite, override with
$ABAPIT_DATA_DIR).
abapit snapshot # save current state (add --skip-applecare on huge fleets)
abapit changes # what changed between the two latest snapshots
abapit changes --json # machine-readable, for scripts/alerts
abapit snapshot --keep 26 # retention: keep the newest 26 snapshotsThe Changes page in the GUI shows the same diffs — devices added/removed,
MDM assignment moves (Intune → Jamf Pro), status and coverage changes —
and lets you compare any two snapshots. Cron it weekly and you have a fleet
history:
0 7 * * 1 /usr/local/bin/abapit snapshot --keep 52
Design rule: stale data is never silent. Live pages serve live data;
when snapshots exist they also enable warm start — on a cold cache the
GUI renders instantly from the latest snapshot with a visible "snapshot data
from — refreshing in the background" banner while live data loads
behind it. That's what makes a 20,000-device org open in milliseconds
instead of a minute. The Refresh button always forces a true live fetch.
applecare.csv is served from the latest snapshot when one exists (it's the
expensive one-call-per-device report); add ?live=1 to force a fresh pull.
The file is plain SQLite — query it directly with sqlite3 or
Datasette; devices_view and applecare_view expose
the common fields as real columns.
abapit serve [--demo] [--port 8866] [--no-browser]
abapit export devices -o devices.csv # any resource: users, apps, blueprints…
abapit export devices --demo | head # works against demo data too
abapit snapshot [--skip-applecare] [--keep N]
abapit changes [--json]
abapit assign --server "Jamf Pro" --file serials.txt # DRY RUN: prints the plan
abapit assign --server "Jamf Pro" --file serials.txt --yes # executes, tracks to completion
abapit probe # empirically map what the key's role allows
abapit token # print a bearer token for curl
abapit orgs # list configured orgsApple has no per-key scopes and no permissions API — a key inherits the
role of its API account, set in ABM/ASM under Access Management → Roles. So
abapit maps permissions empirically: the Permissions button in Settings
(or abapit probe) makes one cheap read per category and a can-never-change-
anything write check, and shows you exactly what the key's role allows.
To run tiered access, create two API accounts in ABM — a read-mostly one for daily use and a device-manager one for migrations — and add both as org profiles; switch from the header dropdown.
- Network: binds
127.0.0.1only by default. Requests with an unrecognizedHostheader are rejected (blocks DNS-rebinding attacks), and cross-origin browser POSTs are refused (blocks CSRF against the settings/snapshot endpoints from malicious websites). Binding to anything other than localhost disables Host checking and prints a loud warning — the app deliberately has no login of its own. - Credentials: private keys are stored as separate files under
~/.config/abapit/keys/with0600permissions in a0700directory; the config JSON holds only paths. Keys are validated and canonicalized at add time. Bearer tokens live in memory only and are never logged. - Data at rest: the snapshot database (your full inventory) is
0600in a0700directory. - Egress: the only hosts ever contacted are
account.apple.comandapi-business.apple.com/api-school.apple.com. - Honest limits: anything running as your user can read the key files
— the same trust model as
~/.ssh. The API account can read inventory and reassign devices between MDM services (the tool's only write); revoke/rotate keys any time in Apple Business Manager.
- The only write is device↔MDM assignment, and it never fires blind:
every run is planned first (unknown serials and no-ops are filtered out
and shown), the dry-run preview is the default everywhere, and execution
requires an explicit confirm (web) or
--yes(CLI). Everything else is read-only. Roadmap: blueprint/configuration CRUD. - Responses are cached in memory for 5 minutes per org (the Refresh
button clears it) to stay friendly with Apple's rate limits; 429s are
retried automatically with backoff, honoring
Retry-After. - Scale: listings page at 1,000 items per API call, so a 5,000-device org cold-loads in seconds and a 20,000-device org in under a minute (then it's cached). Tested patterns hold to ~200k items per resource.
- The AppleCare bulk report is one API call per device — fine for hundreds of devices, slow for tens of thousands. Take a snapshot and it's instant thereafter.
- Keep the server on
127.0.0.1(the default). It has no login of its own — binding it to a network interface would expose your org data.
git clone … && cd abapit
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/pytest
.venv/bin/python -m abapit.cli serve --demoThe demo fleet (abapit/demo.py) mirrors the real client's interface, so UI
work never needs live credentials. MIT licensed — issues and PRs welcome.