Using the API¶
Query the stitched macOS app catalog over plain HTTP, from any language.
The Patcher API is a public, read-only catalog of macOS app patching metadata, stitched from Installomator, Homebrew Cask, AutoPkg, and Jamf App Installers into one canonical record per app. The library talks to your Jamf instance, the API serves the shared upstream catalog that Patcher matches against.
You want catalog data from a shell script, or another language.
You need the app metadata for an app without standing up your own ingestion.
You want to check cross-source version drift, or generate Installomator-shaped labels.
See also
Common Requests¶
The catalog is plain HTTP, so reach it from whatever you already script in. Each tab runs the same four calls: list the catalog, pull one app’s version and download URL, generate an Installomator label, and check for cross-source version drift. The patcher package ships a typed wrapper, PatcherAPIClient (its own tab below); everyone else hits the URLs directly.
#!/bin/bash
BASE="https://api.patcherctl.dev"
# List the first page of the catalog
curl -sS "$BASE/apps?limit=10" | jq .
# One app's current version and download URL
curl -sS "$BASE/apps/firefox" | jq '{version: .current_version, url: .download_url}'
# Generate an Installomator label
curl -sS -X POST "$BASE/apps/firefox/generate-label" | jq .
# Check cross-source version drift (null if no drift)
curl -sS "$BASE/apps/firefox/drift" | jq .
import httpx
BASE = "https://api.patcherctl.dev"
with httpx.Client(base_url=BASE) as client:
# List the first page of the catalog
apps = client.get("/apps", params={"limit": 10}).json()
# One app's current version and download URL
firefox = client.get("/apps/firefox").json()
print(firefox["current_version"], firefox["download_url"])
# Generate an Installomator label
label = client.post("/apps/firefox/generate-label").json()
print(label["content"])
# Check cross-source version drift (None if no drift)
drift = client.get("/apps/firefox/drift").json()
if drift:
print(drift["leader"], "leads", drift["laggard"])
The patcher package wraps every endpoint with typed Pydantic models, so you get attribute access (app.current_version) and editor autocomplete. Methods return None rather than raising when a slug isn’t found.
from patcher import PatcherAPIClient
async with PatcherAPIClient() as api:
# List the first page of the catalog
apps = await api.list_apps(limit=10)
# One app's current version and download URL
firefox = await api.get_app("firefox")
print(firefox.current_version, firefox.download_url)
# Generate an Installomator label
label = await api.generate_label("firefox")
print(label.content)
# Check cross-source version drift (None if no drift)
drift = await api.get_app_drift("firefox")
if drift:
print(f"{drift.leader} leads {drift.laggard}")
const BASE = "https://api.patcherctl.dev";
// List the first page of the catalog
const apps = await fetch(`${BASE}/apps?limit=10`).then((r) => r.json());
// One app's current version and download URL
const firefox = await fetch(`${BASE}/apps/firefox`).then((r) => r.json());
console.log(firefox.current_version, firefox.download_url);
// Generate an Installomator label
const label = await fetch(`${BASE}/apps/firefox/generate-label`, {
method: "POST",
}).then((r) => r.json());
console.log(label.content);
// Check cross-source version drift (null if no drift)
const drift = await fetch(`${BASE}/apps/firefox/drift`).then((r) => r.json());
if (drift) console.log(`${drift.leader} leads ${drift.laggard}`);
import Foundation
func get(_ path: String) async throws -> Any {
let url = URL(string: "https://api.patcherctl.dev\(path)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONSerialization.jsonObject(with: data)
}
// List the first page of the catalog
let apps = try await get("/apps?limit=10")
// One app's current version and download URL
let firefox = try await get("/apps/firefox") as! [String: Any]
print(firefox["current_version"] ?? "", firefox["download_url"] ?? "")
// Generate an Installomator label (POST)
var request = URLRequest(url: URL(string: "https://api.patcherctl.dev/apps/firefox/generate-label")!)
request.httpMethod = "POST"
let (labelData, _) = try await URLSession.shared.data(for: request)
let label = try JSONSerialization.jsonObject(with: labelData) as! [String: Any]
print(label["content"] ?? "")
// Check cross-source version drift (null if no drift)
if let drift = try await get("/apps/firefox/drift") as? [String: Any] {
print(drift["leader"] ?? "", "leads", drift["laggard"] ?? "")
}
require "net/http"
require "json"
BASE = "https://api.patcherctl.dev"
# List the first page of the catalog
apps = JSON.parse(Net::HTTP.get(URI("#{BASE}/apps?limit=10")))
# One app's current version and download URL
firefox = JSON.parse(Net::HTTP.get(URI("#{BASE}/apps/firefox")))
puts firefox["current_version"], firefox["download_url"]
# Generate an Installomator label
label = JSON.parse(Net::HTTP.post(URI("#{BASE}/apps/firefox/generate-label"), "").body)
puts label["content"]
# Check cross-source version drift (nil if no drift)
drift = JSON.parse(Net::HTTP.get(URI("#{BASE}/apps/firefox/drift")))
puts "#{drift['leader']} leads #{drift['laggard']}" if drift
A 404 (with a detail field) means the slug isn’t in the catalog. For the full cookbook, source payloads, drift, pagination, ETag revalidation, and error shapes, see Examples.
Caching¶
The catalog only changes when a fresh build deploys, usually once a day. So most of the time, the data you fetched earlier is still current and you don’t need to download it again. Two things make that cheap.
Cloudflare¶
The API sits behind Cloudflare’s edge cache, so even a plain request usually resolves from a server near you instead of the origin. You get fast responses without doing anything.
See also
For a worked ETag round-trip in curl, see Examples.
Running Your Own¶
The hosted instance is the easiest path, but the API is open source and self-hostable. To run your own catalog (your own ingestion schedule, your own data, or an air-gapped deployment), see Self-Hosting. PatcherAPIClient accepts a base_url= override to point at a self-hosted instance or a local make serve-api run.