This repository is a playground for learning and testing the Apple Business Manager (ABM) API. It focuses on the end-to-end process of:
- Creating a client assertion (JWT) signed with your ABM API private key
- Exchanging that assertion for an OAuth 2.0 access token
- Calling the Apple Business Manager Device Management API (e.g.
/v1/orgDevices)
Along the way, it documents the gotchas around Apple’s certificate/key workflow, including how to decrypt and convert the private key with OpenSSL so Python can use it.
-
make_assertion.py
Builds and prints a short-lived ES256 JWT client assertion using your ABM API credentials and unencrypted PKCS#8 EC private key. -
abm_verify.py
Uses your ABM credentials and unencrypted PKCS#8 EC private key to:- Build an ES256 client assertion
- Exchange it for an access token
- Call the
/v1/orgDevicesendpoint and print the result
This script is a simple “does it all work?” verification tool.
In Apple Business Manager you must:
- Sign in with an account that can manage API access.
- Create an API client (sometimes labeled as “Device Management API” or similar).
- When you create the client, Apple will give you:
- Client ID (e.g.
BUSINESSAPI.6af2be19-…) - Key ID (a GUID-like value)
- A downloadable key file / certificate (this is where the OpenSSL conversion comes in)
- Client ID (e.g.
Important: Apple’s key material is typically encrypted or wrapped. Python’s
cryptographylibrary requires an unencrypted PKCS#8 EC P-256 key in PEM format. The steps below describe how to get there.
The scripts expect ABM_KEY_PATH to point to an unencrypted PKCS#8 EC P-256 private key in PEM format.
Depending on what Apple gives you, you may have either:
- A
.p12/.pfxfile (PKCS#12, password-protected) - A password-protected PEM
- Or a plain
.p8private key that’s already usable
Below are common OpenSSL flows. You may only need one or two of these examples.
- Export the private key from the PKCS#12 bundle (will likely prompt for the export password):
openssl pkcs12 -in abm_client.p12 -nocerts -nodes -out abm_key.pem- Convert to unencrypted PKCS#8 PEM (the format the scripts expect):
openssl pkcs8 -topk8 -inform PEM -outform PEM -in abm_key.pem -out abm_key_unencrypted.pem -nocryptNow set ABM_KEY_PATH to the full path of abm_key_unencrypted.pem.
If you already have a PEM key that’s encrypted with a passphrase:
openssl pkcs8 -topk8 -inform PEM -outform PEM -in encrypted_key.pem -out abm_key_unencrypted.pem -nocryptOpenSSL will prompt you for the passphrase, and produce an unencrypted PKCS#8 PEM.
If Apple gives you a .p8 key that looks like this:
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
…it may already be an unencrypted PKCS#8 EC key. In that case you can usually rename it and use it directly:
cp AuthKey_XXXXXXX.p8 abm_key_unencrypted.pemIf Python later complains that it can’t deserialize the key, convert it with the pkcs8 -topk8 command as shown above.
These scripts require:
- Python 3.9+ (3.11+ recommended)
- The following Python packages:
pip install PyJWT cryptography requestsYou can install them globally or inside a virtualenv/venv—your choice.
Both scripts use the same three environment variables:
-
ABM_CLIENT_ID
The Client ID Apple shows when you create the API client
(e.g.BUSINESSAPI.6af2be19-...) -
ABM_KEY_ID
The Key ID associated with your ABM API key (a GUID-like string) -
ABM_KEY_PATH
The absolute or relative path to your unencrypted PKCS#8 EC P-256 private key file
(e.g./Users/you/keys/abm_key_unencrypted.pem)
export ABM_CLIENT_ID="BUSINESSAPI.6af2be19-..."
export ABM_KEY_ID="93d2de38-..."
export ABM_KEY_PATH="/Users/you/keys/abm_key_unencrypted.pem"You can put those in your shell profile (~/.zshrc) or just export them in the terminal before running the scripts.
This script builds and prints a client assertion JWT. It’s a good first step to confirm:
- Your key is readable
- Your environment variables are correct
- The JWT looks sane and decodes as expected
- Loads the private key from
ABM_KEY_PATHusing thecryptographylibrary - Builds a JWT with:
iss: yourABM_CLIENT_IDsub: yourABM_CLIENT_IDaud: Apple’s token URL (https://account.apple.com/auth/oauth2/token)iat: current UNIX timestampexp:iat + 300seconds (5 minutes)jti: a random UUID
- Signs it with ES256 using your private key and
ABM_KEY_IDas thekidheader
python make_assertion.pyIf everything is configured correctly, it prints a compact JWT string to stdout (one long line of eyJhbGciOi...).
You can paste that JWT into a tool like jwt.io to inspect the header and claims and confirm:
algisES256kidmatches yourABM_KEY_IDissandsubare yourABM_CLIENT_IDaudishttps://account.apple.com/auth/oauth2/token
This is the “full flow” script: it builds the assertion, exchanges it for an access token, and hits the ABM API.
-
Builds a client assertion (ES256, 15-minute lifetime) using:
iss=ABM_CLIENT_IDsub=ABM_CLIENT_IDaud=https://account.apple.com/auth/oauth2/v2/tokeniat= nowexp= now + 15 minutesjti= random UUID
-
Exchanges the assertion for an access token
Sends a POST to Apple’s token endpoint:- URL:
https://account.apple.com/auth/oauth2/token - Content-Type:
application/x-www-form-urlencoded - Params:
grant_type=client_credentialsclient_id=<your ABM_CLIENT_ID>client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearerclient_assertion=<JWT from step 1>scope=business.api
On success, Apple returns a JSON object containing an
access_token. - URL:
-
Calls the ABM API
Uses the access token to call:GET https://api-business.apple.com/v1/orgDevices- Header:
Authorization: Bearer <access_token>
-
Prints the status codes and bodies to your terminal so you can see exactly what Apple returns during both token and API calls.
python abm_verify.pyYou should see output similar to:
Token status: 200
Token body: {"access_token":"...","token_type":"Bearer","expires_in":900,...}
Devices status: 200
{"data":[ ... orgDevices payload ... ]}
If you get 401, 403, or another error, see the troubleshooting section below.
If Python throws an error like:
Could not deserialize key data. The data may be in an incorrect format, the provided password may be incorrect, or it may be an unsupported key type.
Check:
ABM_KEY_PATHis correct- The file is unencrypted (no passphrase required)
- The file is PKCS#8 (
-----BEGIN PRIVATE KEY-----) rather than legacy PKCS#1 or other formats
If needed, re-run the OpenSSL conversion to ensure -nocrypt and -topk8 were used.
Typical causes:
ABM_CLIENT_IDdoesn’t match the client associated with your keyABM_KEY_IDis wrong or doesn’t match the key file- The JWT
audclaim doesn’t match the token URL Apple expects - System clock skew: if your machine’s time is far off,
iat/expmay be invalid
Things to verify:
- Re-copy
ABM_CLIENT_IDandABM_KEY_IDfrom ABM - Make sure the client assertion lifetime is short (5–15 minutes) and uses the proper
aud - Make sure your host time is reasonably accurate (e.g., via system time sync)
If the token request succeeds but the devices request fails:
- Confirm the API client in ABM is granted the correct scopes/permissions
- Ensure that you’re using
scope=business.apiin the token request - Double-check that your ABM account actually has devices and that your organization is configured for the Device Management API
When running the OpenSSL commands:
- Ensure the paths are correct and quoted if they contain spaces
- If you see
Can't open input file, check that the source file exists - If you see
Can't open output file, make sure the directory is writable
Example with spaces in the path:
openssl pkcs12 -in "/Users/you/ABM Keys/abm_client.p12" -nocerts -nodes -out "/Users/you/ABM Keys/abm_key.pem"Conceptually, this playground is modeling the standard OAuth 2.0 client_credentials + JWT client authentication flow that Apple uses:
-
ABM issues you a credential set:
Client IDKey ID- Encrypted key/cert material
-
You convert the key into a runtime-usable format:
- Unencrypted PKCS#8 EC P-256 PEM
-
Your client (these scripts) builds a signed JWT:
- Claims: who you are (
iss/sub), who it’s for (aud), validity period (iat/exp), unique ID (jti) - Header: algorithm (
ES256), key id (kid)
- Claims: who you are (
-
You exchange that JWT for an access token:
- POST to Apple’s token endpoint
- Get back a short-lived bearer token
-
You call the ABM API with that token:
Authorization: Bearer <access_token>- Hit endpoints like
/v1/orgDevices
This repo keeps each step small and inspectable, so you can learn what’s happening at each stage rather than hiding everything in a single monolithic script.