Skip to main content
Question

Populate Jamf Pro Warranty Information with ABM/ASM API + MUT

  • February 23, 2026
  • 12 replies
  • 781 views

bwoods
Forum|alt.badge.img+14

With Apple recently expanding access to the ABM/ASM API, I set out to recreate the GSX experience. By leveraging Jamf’s new MCP feature, Claude Code, and MUT, I believe I’ve come very close. Additional details are outlined below.

Please visit the following Github repo for the latest version of Warranty Wrangler: https://github.com/brndnwds6/Warranty-Wrangler/tree/main

#!/bin/zsh

# ==============================================================================
# Script Name: warranty_wrangler.zsh
# Author: Brandon Woods
# Date: February 23, 2026
#
# Changelog:
# February 25, 2026 — Added AppleCare+ support. Warranty Expires now reflects
# the AppleCare+ expiration date when active coverage exists,
# falling back to the Limited Warranty date for devices
# without AppleCare. Credit: fpatafta (Jamf Nation Community)
# February 26, 2026 — Added Apple School Manager (ASM) support via --asm flag.
# Switches API base URL and OAuth scope automatically.
# Credit: MultiSiggloo (Jamf Nation Community)
# ==============================================================================
#
# Pulls device and AppleCare / warranty coverage data from Apple Business
# Manager (ABM) or Apple School Manager (ASM) and writes two MUT-compatible CSV files:
#
# ComputerTemplate.csv — Mac devices (productFamily = "Mac")
# MobileDeviceTemplate.csv — iPhone, iPad, Apple TV, iPod, Vision Pro, etc.
#
# Incremental mode:
# If the output CSV files already exist at the configured paths, the script
# loads the serials already present and skips them — only newly added ABM
# devices are fetched and appended. If no new devices are found the script
# exits and tells you so.
#
# Populated fields (all others left blank):
# Both templates:
# Serial Number — device serial / ID from ABM
# PO Number — orderNumber from ABM device record
# Vendor — purchaseSourceType from ABM device record
# Purchase Price — not available in ABM API (always blank)
# PO Date — orderDateTime from ABM device record (date only)
# Warranty Expires — Limited Warranty endDateTime (date only)
# AppleCare ID — AppleCare agreement number (blank if none)
#
# Each device row is written to disk immediately after its coverage is fetched.
#
# Prerequisites:
# - ABM API account with Client ID, Key ID, and .pem private key
# - jq (brew install jq)
# - openssl + xxd (built-in on macOS)
#
# Usage:
# ./warranty_wrangler.zsh
# ./warranty_wrangler.zsh --key /path/to/key.pem \
# --client-id BUSINESSAPI.xxxx \
# --key-id xxxx \
# --outdir /path/to/output/folder \
# --computer-file MyMacs.csv \
# --mobile-file MyMobileDevices.csv
# ./warranty_wrangler.zsh --asm \
# --client-id SCHOOLAPI.xxxx \
# --key-id xxxx
# ==============================================================================

# ---------- Configuration (edit these) ---------------------------------------
ABM_PRIVATE_KEY_PATH="/path/to/private-key.pem"
ABM_CLIENT_ID="BUSINESSAPI.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
ABM_KEY_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
OUTPUT_DIR="."
COMPUTER_FILENAME="ComputerTemplate.csv"
MOBILE_FILENAME="MobileDeviceTemplate.csv"

# API endpoints — overridden automatically when --asm flag is used
ABM_AUTH_URL="https://account.apple.com/auth/oauth2/token"
ABM_API_BASE="https://api-business.apple.com/v1"
ABM_SCOPE="business.api"
ASM_MODE=false

# Pause between per-device coverage API calls to avoid rate limiting (seconds)
RATE_LIMIT_DELAY=0.2

# ---------- Parse command-line flags -----------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--key) ABM_PRIVATE_KEY_PATH="$2"; shift 2 ;;
--client-id) ABM_CLIENT_ID="$2"; shift 2 ;;
--key-id) ABM_KEY_ID="$2"; shift 2 ;;
--outdir) OUTPUT_DIR="$2"; shift 2 ;;
--computer-file) COMPUTER_FILENAME="$2"; shift 2 ;;
--mobile-file) MOBILE_FILENAME="$2"; shift 2 ;;
--delay) RATE_LIMIT_DELAY="$2"; shift 2 ;;
--asm) ASM_MODE=true; shift 1 ;;
--help|-h)
sed -n '3,38p' "$0" | sed 's/^# \{0,1\}//'
exit 0 ;;
*)
echo "Unknown option: $1" >&2
exit 1 ;;
esac
done

# Ensure filenames end in .csv
[[ "$COMPUTER_FILENAME" != *.csv ]] && COMPUTER_FILENAME="${COMPUTER_FILENAME}.csv"
[[ "$MOBILE_FILENAME" != *.csv ]] && MOBILE_FILENAME="${MOBILE_FILENAME}.csv"

# Override API base and scope for Apple School Manager
if [[ "$ASM_MODE" == true ]]; then
ABM_API_BASE="https://api-school.apple.com/v1"
ABM_SCOPE="school.api"
echo "-> Mode: Apple School Manager (ASM)"
else
echo "-> Mode: Apple Business Manager (ABM)"
fi

COMPUTER_CSV="${OUTPUT_DIR}/${COMPUTER_FILENAME}"
MOBILE_CSV="${OUTPUT_DIR}/${MOBILE_FILENAME}"

# ---------- Dependency checks -------------------------------------------------
for cmd in jq openssl curl xxd; do
if ! command -v "$cmd" &>/dev/null; then
echo "ERROR: '$cmd' is required but not found. Install with: brew install $cmd" >&2
exit 1
fi
done

if [[ ! -f "$ABM_PRIVATE_KEY_PATH" ]]; then
echo "ERROR: Private key not found at: $ABM_PRIVATE_KEY_PATH" >&2
exit 1
fi

if [[ ! -d "$OUTPUT_DIR" ]]; then
echo "ERROR: Output directory does not exist: $OUTPUT_DIR" >&2
exit 1
fi

# ---------- Detect existing CSVs and load known serials ----------------------
# knownComputerSerials and knownMobileSerials are associative arrays used as
# sets — key = serial number, value = 1. Lookup is O(1).
typeset -A knownComputerSerials
typeset -A knownMobileSerials

computerFileExists=false
mobileFileExists=false

if [[ -f "$COMPUTER_CSV" ]]; then
computerFileExists=true
# Read col 1 (serial), skip header row, strip surrounding quotes
while IFS=, read -r serial rest; do
serial="${serial//\"/}"
[[ -n "$serial" && "$serial" != "Computer Serial" ]] && knownComputerSerials[$serial]=1
done < "$COMPUTER_CSV"
echo "-> Existing computer file detected: $COMPUTER_CSV"
echo " ${#knownComputerSerials} known serials loaded — will append new devices only"
else
echo "-> No existing computer file found — will create: $COMPUTER_CSV"
fi

if [[ -f "$MOBILE_CSV" ]]; then
mobileFileExists=true
while IFS=, read -r serial rest; do
serial="${serial//\"/}"
[[ -n "$serial" && "$serial" != "Mobile Device Serial" ]] && knownMobileSerials[$serial]=1
done < "$MOBILE_CSV"
echo "-> Existing mobile file detected: $MOBILE_CSV"
echo " ${#knownMobileSerials} known serials loaded — will append new devices only"
else
echo "-> No existing mobile file found — will create: $MOBILE_CSV"
fi

# ---------- Helper: base64url encode -----------------------------------------
base64url() {
openssl base64 -A | tr '+/' '-_' | tr -d '='
}

# ---------- Step 1: Build and sign the JWT client assertion -------------------
echo "-> Generating JWT client assertion..."

nowTimestamp=$(date -u +%s)
expTimestamp=$(( nowTimestamp + 15552000 )) # 180 days
jti=$(uuidgen | tr '[:upper:]' '[:lower:]')

headerJson=$(printf '{"alg":"ES256","kid":"%s","typ":"JWT"}' "$ABM_KEY_ID")
jwtHeader=$(printf '%s' "$headerJson" | base64url)

payloadJson=$(printf '{"sub":"%s","aud":"https://account.apple.com/auth/oauth2/v2/token","iat":%d,"exp":%d,"jti":"%s","iss":"%s"}' \
"$ABM_CLIENT_ID" "$nowTimestamp" "$expTimestamp" "$jti" "$ABM_CLIENT_ID")
jwtPayload=$(printf '%s' "$payloadJson" | base64url)

signingInput="${jwtHeader}.${jwtPayload}"

asn1Out=$(printf '%s' "$signingInput" \
| openssl dgst -sha256 -sign "$ABM_PRIVATE_KEY_PATH" 2>/dev/null \
| openssl asn1parse -inform DER 2>&1)

if [[ $? -ne 0 ]]; then
echo "ERROR: openssl signing failed. Verify your .pem contains a valid EC private key." >&2
echo "$asn1Out" >&2
exit 1
fi

rHex=$(echo "$asn1Out" | awk '/INTEGER/{gsub(/.*INTEGER[[:space:]]+:/,"",$0); gsub(/ /,"",$0); if(++n==1) print}')
sHex=$(echo "$asn1Out" | awk '/INTEGER/{gsub(/.*INTEGER[[:space:]]+:/,"",$0); gsub(/ /,"",$0); if(++n==2) print}')

if [[ -z "$rHex" || -z "$sHex" ]]; then
echo "ERROR: Failed to extract r/s from ASN.1 signature." >&2
echo "$asn1Out" >&2
exit 1
fi

rHex=$(printf '%s' "$rHex" | sed 's/^00//')
sHex=$(printf '%s' "$sHex" | sed 's/^00//')
while [[ ${#rHex} -lt 64 ]]; do rHex="00${rHex}"; done
while [[ ${#sHex} -lt 64 ]]; do sHex="00${sHex}"; done

signature=$(printf '%s%s' "$rHex" "$sHex" | xxd -r -p | base64url)
clientAssertion="${signingInput}.${signature}"
echo " OK Client assertion generated"

# ---------- Step 2: Exchange client assertion for bearer token ---------------
echo "-> Requesting bearer token..."

tokenResponse=$(curl -s -w "\n__STATUS__%{http_code}" -X POST \
-H "Host: account.apple.com" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${ABM_AUTH_URL}?grant_type=client_credentials&client_id=${ABM_CLIENT_ID}&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=${clientAssertion}&scope=${ABM_SCOPE}")

httpStatus=$(echo "$tokenResponse" | grep '__STATUS__' | sed 's/__STATUS__//')
tokenBody=$(echo "$tokenResponse" | grep -v '__STATUS__')

if [[ "$httpStatus" != "200" ]]; then
echo "ERROR: Token request failed (HTTP $httpStatus):" >&2
echo "$tokenBody" >&2
exit 1
fi

accessToken=$(echo "$tokenBody" | jq -r '.access_token // empty')
if [[ -z "$accessToken" ]]; then
echo "ERROR: No access_token in response:" >&2
echo "$tokenBody" >&2
exit 1
fi

echo " OK Bearer token obtained (valid ~1 hour)"

# ---------- Step 3: Initialize output files -----------------------------------
# Write header only if the file does not already exist
echo "-> Initializing output files..."

if [[ "$computerFileExists" == false ]]; then
printf '%s\n' "Computer Serial,Display Name,Asset Tag,Barcode 1,Barcode 2,Username,Real Name,Email Address,Position,Phone Number,Department,Building,Room,PO Number,Vendor,Purchase Price,PO Date,Warranty Expires,Is Leased,Lease Expires,AppleCare ID,Site (ID or Name)" > "$COMPUTER_CSV"
echo " Created: $COMPUTER_CSV"
else
echo " Appending to: $COMPUTER_CSV"
fi

if [[ "$mobileFileExists" == false ]]; then
printf '%s\n' "Mobile Device Serial,Display Name,Enforce Name,Asset Tag,Username,Real Name,Email Address,Position,Phone Number,Department,Building,Room,PO Number,Vendor,Purchase Price,PO Date,Warranty Expires,Is Leased,Lease Expires,AppleCare ID,Airplay Password (tvOS Only),Site (ID or Name)" > "$MOBILE_CSV"
echo " Created: $MOBILE_CSV"
else
echo " Appending to: $MOBILE_CSV"
fi

# ---------- Step 4: Enumerate devices and fetch coverage page by page --------
echo "-> Fetching devices from ABM..."

totalDevices=0
newComputerCount=0
newMobileCount=0
skippedCount=0
errorCount=0
nextCursor=""
pageCount=0

while true; do
pageCount=$(( pageCount + 1 ))

if [[ -n "$nextCursor" ]]; then
pageUrl="${ABM_API_BASE}/orgDevices?cursor=${nextCursor}"
else
pageUrl="${ABM_API_BASE}/orgDevices"
fi

pageResponse=$(curl -sf \
-H "Authorization: Bearer ${accessToken}" \
"$pageUrl")

if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to fetch device list (page $pageCount)" >&2
exit 1
fi

pageDeviceCount=$(echo "$pageResponse" | jq '.data | length')
echo " Page $pageCount: $pageDeviceCount devices"

while IFS= read -r serial \
&& IFS= read -r productFamily \
&& IFS= read -r orderNumber \
&& IFS= read -r purchaseSourceType \
&& IFS= read -r orderDateTime; do

totalDevices=$(( totalDevices + 1 ))

# Skip devices already present in the existing CSV
if [[ "$productFamily" == "Mac" ]]; then
if (( ${+knownComputerSerials[$serial]} )); then
skippedCount=$(( skippedCount + 1 ))
continue
fi
targetCSV="$COMPUTER_CSV"
newComputerCount=$(( newComputerCount + 1 ))
else
if (( ${+knownMobileSerials[$serial]} )); then
skippedCount=$(( skippedCount + 1 ))
continue
fi
targetCSV="$MOBILE_CSV"
newMobileCount=$(( newMobileCount + 1 ))
fi

# Normalize null jq values to empty string
[[ "$orderNumber" == "null" ]] && orderNumber=""
[[ "$purchaseSourceType" == "null" ]] && purchaseSourceType=""
[[ "$orderDateTime" == "null" ]] && orderDateTime=""

# Trim time portion from ISO 8601 timestamp — keep date only
poDate="${orderDateTime%%T*}"

echo " New device: $serial ($productFamily)"

# Fetch AppleCare coverage for this device
coverageResponse=$(curl -sf \
-H "Authorization: Bearer ${accessToken}" \
"${ABM_API_BASE}/orgDevices/${serial}/appleCareCoverage")

if [[ $? -ne 0 ]]; then
# Coverage unavailable — write serial and PO fields, leave warranty blank
if [[ "$productFamily" == "Mac" ]]; then
printf '"%s",,,,,,,,,,,,,"%s","%s",,"%s",,,,,\n' \
"$serial" "$orderNumber" "$purchaseSourceType" "$poDate" >> "$targetCSV"
else
printf '"%s",,,,,,,,,,,,"%s","%s",,"%s",,,,,,\n' \
"$serial" "$orderNumber" "$purchaseSourceType" "$poDate" >> "$targetCSV"
fi
errorCount=$(( errorCount + 1 ))
sleep "$RATE_LIMIT_DELAY"
continue
fi

# Warranty Expires — prefer active AppleCare+ expiration date when available,
# fall back to Limited Warranty end date if no active AppleCare coverage exists.
# Credit: fpatafta (Jamf Nation Community, February 25, 2026)
warrantyExpires=$(echo "$coverageResponse" | jq -r '
[ .data[] | select(.attributes.description != "Limited Warranty" and .attributes.status == "ACTIVE") ]
| first
| .attributes.endDateTime // ""')

if [[ -z "$warrantyExpires" || "$warrantyExpires" == "null" ]]; then
warrantyExpires=$(echo "$coverageResponse" | jq -r '
[ .data[] | select(.attributes.description == "Limited Warranty") ]
| first
| .attributes.endDateTime // ""')
fi
warrantyExpires="${warrantyExpires%%T*}"

# AppleCare agreement number -> AppleCare ID (prefer ACTIVE entry)
applecareID=$(echo "$coverageResponse" | jq -r '
[ .data[] | select(.attributes.description != "Limited Warranty") ]
| sort_by(.attributes.status == "ACTIVE" | not)
| first
| .attributes.agreementNumber // ""')

# Write row immediately — Purchase Price always blank (not in ABM API)
if [[ "$productFamily" == "Mac" ]]; then
# Computer: 22 cols
# Col: 1=Serial 14=PO# 15=Vendor 16=Price(blank) 17=PODate 18=WarrantyExpires 21=AppleCareID
printf '"%s",,,,,,,,,,,,,"%s","%s",,"%s","%s",,,"%s",\n' \
"$serial" \
"$orderNumber" "$purchaseSourceType" \
"$poDate" "$warrantyExpires" \
"$applecareID" >> "$targetCSV"
else
# Mobile: 22 cols
# Col: 1=Serial 13=PO# 14=Vendor 15=Price(blank) 16=PODate 17=WarrantyExpires 20=AppleCareID
printf '"%s",,,,,,,,,,,,"%s","%s",,"%s","%s",,,"%s",,\n' \
"$serial" \
"$orderNumber" "$purchaseSourceType" \
"$poDate" "$warrantyExpires" \
"$applecareID" >> "$targetCSV"
fi

sleep "$RATE_LIMIT_DELAY"

done < <(echo "$pageResponse" | jq -r '.data[] | (
.id,
(.attributes.productFamily // "Unknown"),
(.attributes.orderNumber // "null"),
(.attributes.purchaseSourceType // "null"),
(.attributes.orderDateTime // "null")
)')

echo " Page $pageCount complete — New: $newComputerCount computers, $newMobileCount mobile | Skipped: $skippedCount | Errors: $errorCount"

nextCursor=$(echo "$pageResponse" | jq -r '.meta.paging.nextCursor // empty')
[[ -z "$nextCursor" ]] && break
done

# ---------- Summary -----------------------------------------------------------
newDevicesTotal=$(( newComputerCount + newMobileCount ))

echo ""
echo "============================================"
if [[ "$ASM_MODE" == true ]]; then
echo " ASM Warranty Recon Complete"
else
echo " ABM Warranty Recon Complete"
fi
echo "============================================"
echo " Total devices in ABM : $totalDevices"
echo " Already in CSV : $skippedCount (skipped)"
echo " New computers added : $newComputerCount -> $(basename "$COMPUTER_CSV")"
echo " New mobile added : $newMobileCount -> $(basename "$MOBILE_CSV")"
echo " Coverage errors : $errorCount"
echo "============================================"

if [[ $newDevicesTotal -eq 0 ]]; then
echo ""
if [[ "$ASM_MODE" == true ]]; then
echo " No new devices were found in ASM."
else
echo " No new devices were found in ABM."
fi
echo " Both CSV files are already up to date."
echo "============================================"
fi

Warranty Wrangler — Setup & Usage Guide

This guide walks through everything needed to run warranty_wrangler.zsh, a script that pulls warranty and AppleCare+ coverage data from Apple Business Manager (ABM) or Apple School Manager (ASM) and produces CSV files ready for import into the Mass Update Tool (MUT).

Prerequisites

  • A Mac running macOS
  • Administrator access to Apple Business Manager or Apple School Manager
  • jq installed — if you don't have it, install it with Homebrew:
    brew install jq
  • openssl, curl, and xxd — all included with macOS by default

Step 1 — Create a Working Directory

Create a dedicated folder to store the script, your private key, and the generated CSV files. Keeping everything together makes the script easier to configure and run.

Open Terminal and run:

mkdir ~/abm-warranty

You can name and place this folder wherever makes sense for your environment. Just note the full path — you'll need it shortly.

Step 2 — Create an API Account

Note: You must have the Administrator role to complete this step.

Apple Business Manager

  1. Sign in to Apple Business Manager.
  2. Select your name at the bottom of the sidebar, then select Preferences.
  3. Select API from the preferences panel.
  4. Select Get Started, enter a name for the account (e.g., Warranty Report), then select Create.
  5. Select Generate Private Key. A .pem file will automatically download to your browser's download location.
  6. Select Manage on the newly created API account and note the following two values:
    • Client ID — looks like BUSINESSAPI.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    • Key ID — looks like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

For full details, refer to Apple's official documentation: Create an API account in Apple Business Manager

Apple School Manager

The process is identical — sign in to Apple School Manager, navigate to Preferences → API, and follow the same steps. Your Client ID will be prefixed with SCHOOLAPI. instead of BUSINESSAPI.

Important: The .pem file can only be downloaded once. If it is lost, you will need to revoke the key and generate a new one. Store it securely.

Step 3 — Store the Private Key

Move the downloaded .pem file into the working directory you created in Step 1.

In Terminal:

mv ~/Downloads/your-private-key.pem ~/abm-warranty/

Replace your-private-key.pem with the actual filename of the downloaded file.

Step 4 — Place the Script in the Working Directory

Move or copy warranty_wrangler.zsh into the same working directory:

mv ~/Downloads/warranty_wrangler.zsh ~/abm-warranty/

Then make it executable:

chmod +x ~/abm-warranty/warranty_wrangler.zsh

Step 5 — Configure the Script

Open the script in a text editor to fill in your credentials and paths. You can use:

  • Terminal with a built-in editor:
    nano ~/abm-warranty/warranty_wrangler.zsh
  • Visual Studio Code:
    code ~/abm-warranty/warranty_wrangler.zsh
  • CodeRunner — open the file from the working directory

Find the configuration block near the top of the script:

# ---------- Configuration (edit these) ---------------------------------------
ABM_PRIVATE_KEY_PATH="/path/to/private-key.pem"
ABM_CLIENT_ID="BUSINESSAPI.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
ABM_KEY_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
OUTPUT_DIR="."
COMPUTER_FILENAME="ComputerTemplate.csv"
MOBILE_FILENAME="MobileDeviceTemplate.csv"
ASM_MODE=false

Replace each placeholder with your actual values:

Variable What to enter
ABM_PRIVATE_KEY_PATH Full path to your .pem file, e.g. /Users/yourname/abm-warranty/private-key.pem
ABM_CLIENT_ID The Client ID from your ABM or ASM API account
ABM_KEY_ID The Key ID from your ABM or ASM API account
OUTPUT_DIR Path to the folder where CSVs should be saved, e.g. /Users/yourname/abm-warranty
COMPUTER_FILENAME Name for the Mac CSV file (default: ComputerTemplate.csv)
MOBILE_FILENAME Name for the mobile device CSV file (default: MobileDeviceTemplate.csv)
ASM_MODE Set to true if using Apple School Manager, leave as false for Apple Business Manager

Save and close the file when done.

Step 6 — Run the Script

Running from Terminal

Navigate to your working directory and run the script:

cd ~/abm-warranty
./warranty_wrangler.zsh

For Apple School Manager, pass the --asm flag:

./warranty_wrangler.zsh --asm

All configurable options are also available as flags:

./warranty_wrangler.zsh \
--key /path/to/private-key.pem \
--client-id BUSINESSAPI.xxxx \
--key-id xxxx \
--outdir /path/to/output \
--computer-file MyMacs.csv \
--mobile-file MyMobileDevices.csv

Running from CodeRunner

If you prefer to run the script directly in CodeRunner, configure everything in the configuration block at the top of the script rather than using flags. Set ASM_MODE=true to run against Apple School Manager, or leave it as false for Apple Business Manager. No flags are needed.

The script will print its progress as it runs, showing each page of devices fetched and a count of new vs. skipped devices per page.

What the Script Does

Two separate output files

The script separates devices into two CSV files:

  • ComputerTemplate.csv — contains all Mac computers found in ABM or ASM
  • MobileDeviceTemplate.csv — contains all other Apple devices: iPhones, iPads, Apple TVs, iPod touches, Apple Vision Pro, and any other non-Mac products

Each row is written to disk immediately as it is processed, so if the script is interrupted, any data already fetched is preserved.

Fields populated

Both files include the following fields for each device (all other columns are left blank):

Field Source
Serial Number Device serial number from ABM/ASM
PO Number Order number from ABM/ASM device record
Vendor Purchase source from ABM/ASM device record
Purchase Price Not available in the API — always blank
PO Date Order date from ABM/ASM (date only)
Warranty Expires AppleCare+ expiration if active, otherwise Limited Warranty end date
AppleCare ID AppleCare agreement number, if the device has AppleCare coverage

AppleCare+ support

The Warranty Expires field prioritizes the AppleCare+ expiration date when a device has active AppleCare+ coverage. If no active AppleCare+ coverage exists, it falls back to the standard Limited Warranty end date. This means the field always reflects the longest applicable coverage window for each device.

Note: If you previously ran the script before AppleCare+ support was added, existing devices in your CSVs will still have the old Limited Warranty dates. To re-fetch corrected dates for those devices, delete or rename your existing CSV files and run the script again so all devices are treated as new.

Incremental updates

The script is designed to be run repeatedly as new devices are added to ABM or ASM. When run again:

  • If the output CSV files already exist at the configured paths, the script reads the serial numbers already present in each file
  • It then compares those against all devices currently in ABM/ASM
  • Only new devices not already in the CSV are fetched and appended — existing rows are never modified
  • If no new devices are found, the script exits with a clear message confirming that both files are already up to date

This means you can run the script on a regular schedule and the CSVs will grow over time to reflect your fleet without duplication.

Apple School Manager vs. Apple Business Manager

The script supports both platforms. The API endpoints and OAuth scope differ between ABM and ASM — the script handles this automatically based on the --asm flag or the ASM_MODE variable.

  ABM ASM
Client ID prefix BUSINESSAPI. SCHOOLAPI.
API base URL api-business.apple.com api-school.apple.com
OAuth scope business.api school.api
Flag / variable default --asm or ASM_MODE=true

Both platforms use the same private key format, authentication flow, device data structure, and coverage endpoints — only the two values above differ.

Using the CSVs with MUT

The generated CSV files are formatted to match MUT's default templates exactly — the column headers and order are identical to what MUT expects out of the box.

To import into Jamf Pro using MUT:

  1. Open MUT and connect to your Jamf Pro server
  2. For Mac warranty data, select Computers and import ComputerTemplate.csv
  3. For mobile device and Apple TV warranty data, select Mobile Devices and import MobileDeviceTemplate.csv
  4. MUT will match each row to the device by serial number and update only the fields that have values — blank columns in the CSV are ignored

Note: MUT updates records by matching the serial number column. Devices in the CSV that do not exist in Jamf Pro will be skipped by MUT without causing errors.

Customizing Output Filenames

If you want to use different filenames for the generated CSVs — for example, to include a date or distinguish between environments — you can pass them as flags when running the script:

./warranty_wrangler.zsh \
--computer-file "Macs_$(date +%Y-%m-%d).csv" \
--mobile-file "Mobile_$(date +%Y-%m-%d).csv"

You can also change COMPUTER_FILENAME and MOBILE_FILENAME directly in the configuration block at the top of the script.

Note: If you change the filename between runs, the script will not recognize the old file and will treat all devices as new. Use consistent filenames across runs to take advantage of the incremental update behavior.

Troubleshooting

"Private key not found" — double-check that ABM_PRIVATE_KEY_PATH points to the exact location of your .pem file, including the filename and extension.

"Token request failed" — verify that your ABM_CLIENT_ID and ABM_KEY_ID match exactly what is shown in the API account management screen. Both values are case-sensitive. If using ASM, confirm that ASM_MODE=true is set or the --asm flag was passed — using a SCHOOLAPI. Client ID without ASM mode enabled will cause an auth failure.

"jq not found" — install jq with brew install jq and re-run the script.

Warranty fields are blank in Jamf after MUT import — confirm that the column headers in the CSV match the field names in Jamf Pro exactly. The script uses MUT's default column names, so no changes should be needed on a standard Jamf setup.

Warranty Expires shows the 1-year date instead of AppleCare+ — this means the device either does not have AppleCare+ coverage, or the coverage entry in ABM/ASM does not have a status of ACTIVE. The script falls back to the Limited Warranty date in both cases.

12 replies

mattjerome
Forum|alt.badge.img+11
  • Jamf Heroes
  • February 23, 2026

This is fantastic! Great work!


ktrojano
Forum|alt.badge.img+21
  • Jamf Heroes
  • February 23, 2026

This is fantastic! Great work!

I totally agree and this would make a great JNUC session! I encourage you to apply!


shrisivakumaran
Forum|alt.badge.img+10

Will work on it

 


jbitton
  • Explorer
  • February 25, 2026

Great work, any plans to include the applecare expiry dates as well?

 


FerrisBNA
Forum|alt.badge.img+3
  • Contributor
  • February 25, 2026

With Apple recently expanding access to the ABM/ASM API, I set out to recreate the GSX experience...

This is ninja skills right here.  Thank you for being Awesome!

-Pat


fpatafta
  • Visitor
  • February 25, 2026

Great script ​@bwoods ! Just wanted to share a tweak for anyone who wants the Warranty Expires field to reflect the AppleCare+ expiration date instead of the Limited Warranty date.

The current coverage logic always pulls the Limited Warranty end date:
 

warrantyExpires=$(echo "$coverageResponse" | jq -r '
    [ .data[] | select(.attributes.description == "Limited Warranty") ]
    | first
    | .attributes.endDateTime // ""')


 

So even if a device has AppleCare+, you get the 1-year Limited Warranty date, not the actual AppleCare+ expiration.Here's a drop-in replacement that prefers the AppleCare+ date when available and falls back to Limited Warranty if there's no AppleCare coverage:

 

# try AppleCare+ expiration first (prefer ACTIVE entry)
warrantyExpires=$(echo "$coverageResponse" | jq -r '
    [ .data[] | select(.attributes.description != "Limited Warranty" and .attributes.status == "ACTIVE") ]
    | first
    | .attributes.endDateTime // ""')

# fall back to Limited Warranty if no active AppleCare
if [[ -z "$warrantyExpires" || "$warrantyExpires" == "null" ]]; then
    warrantyExpires=$(echo "$coverageResponse" | jq -r '
        [ .data[] | select(.attributes.description == "Limited Warranty") ]
        | first
        | .attributes.endDateTime // ""')
fi
warrantyExpires="${warrantyExpires%%T*}"

 

Note: if you've already run the script with the old logic, you'll want to delete or rename your existing CSVs so it re-fetches all devices with the corrected dates.


bwoods
Forum|alt.badge.img+14
  • Author
  • Honored Contributor
  • February 25, 2026

Great script ​@bwoods ! Just wanted to share a tweak for anyone who wants the Warranty Expires field to reflect the AppleCare+ expiration date instead of the Limited Warranty date.

The current coverage logic always pulls the Limited Warranty end date:
 

warrantyExpires=$(echo "$coverageResponse" | jq -r '
    [ .data[] | select(.attributes.description == "Limited Warranty") ]
    | first
    | .attributes.endDateTime // ""')


 

So even if a device has AppleCare+, you get the 1-year Limited Warranty date, not the actual AppleCare+ expiration.Here's a drop-in replacement that prefers the AppleCare+ date when available and falls back to Limited Warranty if there's no AppleCare coverage:

 

# try AppleCare+ expiration first (prefer ACTIVE entry)
warrantyExpires=$(echo "$coverageResponse" | jq -r '
    [ .data[] | select(.attributes.description != "Limited Warranty" and .attributes.status == "ACTIVE") ]
    | first
    | .attributes.endDateTime // ""')

# fall back to Limited Warranty if no active AppleCare
if [[ -z "$warrantyExpires" || "$warrantyExpires" == "null" ]]; then
    warrantyExpires=$(echo "$coverageResponse" | jq -r '
        [ .data[] | select(.attributes.description == "Limited Warranty") ]
        | first
        | .attributes.endDateTime // ""')
fi
warrantyExpires="${warrantyExpires%%T*}"

 

Note: if you've already run the script with the old logic, you'll want to delete or rename your existing CSVs so it re-fetches all devices with the corrected dates.

Thank you. I’ve updated the script with your suggestion on Jamf Nation and Github. I’ve also added you to the credits. Feel free to contribute here:  https://github.com/brndnwds6/AMB-Warranty-Report/tree/main


MultiSiggloo
Forum|alt.badge.img+5
  • New Contributor
  • February 26, 2026

Nice work!!
Works flawlessly with ABM.
Something needs to be changed to work with ASM, anyone knows what?


MultiSiggloo
Forum|alt.badge.img+5
  • New Contributor
  • February 26, 2026

Nice work!!
Works flawlessly with ABM.
Something needs to be changed to work with ASM, anyone knows what?

Found it.
Line 64 and 202


bwoods
Forum|alt.badge.img+14
  • Author
  • Honored Contributor
  • March 1, 2026

Nice work!!
Works flawlessly with ABM.
Something needs to be changed to work with ASM, anyone knows what?

Found it.
Line 64 and 202

@MultiSiggloo, I appreciate you pointing that out. I’ve added support for ASM with the --asm flag and the ASM_Mode variable. You have also been credited in the script.


MultiSiggloo
Forum|alt.badge.img+5
  • New Contributor
  • March 5, 2026

Anyone else having problem when there is a lot of machines?

I get “ERROR: Failed to fetch device list” on page 41 when I run for the first time.
If i run again I get the error on page 21.

Any workaround?


bwoods
Forum|alt.badge.img+14
  • Author
  • Honored Contributor
  • March 11, 2026

Anyone else having problem when there is a lot of machines?

I get “ERROR: Failed to fetch device list” on page 41 when I run for the first time.
If i run again I get the error on page 21.

Any workaround?

Hey ​@MultiSiggloo, I just processed about 4500 + ASM devices for a school district without seeing this issue. Are you using the updated version of the script from Github? 

I’ll keep testing with my other clients with large fleets and let you know if I find anything. Also keep in mind that the bearer token from ABM is only available for about an hour. I’ll try to verify if this time limit can be extended.