Endpoints¶
Read the stitched macOS app catalog without standing up your own ingestion pipeline.
Note
The Patcher API catalog is public — no authentication required for any /apps* or /stats endpoint. Admin endpoints used to upload fresh catalog data are gated behind a separate deploy token and not documented publicly.
Two ways in¶
Pick the surface that matches your consumer:
PatcherAPIClientFor Python consumers. Async client wrapping every public endpoint, returning typed Pydantic models. Lives in the patcher package: from patcher import PatcherAPIClient. See examples.
For any-language consumers, shell scripts, or one-off curl checks. The endpoint reference below plus the OpenAPI schema are the full surface.
Base URL¶
https://api.patcherctl.dev
The API is served behind a Cloudflare named tunnel terminating TLS at Cloudflare’s edge. All requests are HTTPS.
Caching¶
Catalog responses carry an ETag whose value is a version token derived from the catalog’s newest update timestamp. The token changes exactly when the catalog data changes (typically once per day) and never otherwise, so it’s a perfect cache key.
ETag: W/"4f7b...e2a1"
Cache-Control: public, max-age=300, stale-while-revalidate=3600
Clients that send If-None-Match matching the current ETag get a 304 Not Modified short-circuit with no body. The header is parsed per RFC 7232: pass a single ETag value, a comma-separated list of ETags (useful when you’ve cached multiple catalog versions and want a hit on any of them), or the wildcard * for unconditional revalidation. Cloudflare also caches across users between deploys, so a hot path typically never reaches the origin. Recommended client pattern: store the ETag on first response, send it back on subsequent requests, accept either 200 + new body or 304 + reuse cached body.
ETag headers are applied to GET requests under /apps* only. /health and admin endpoints bypass.
Endpoint reference¶
The reference below is auto-generated from the live OpenAPI schema. When a route’s signature or response model changes in code, this page follows automatically on the next docs build.
- GET /apps¶
List Apps
List apps in the catalog with optional filters and pagination.
All filters (
vendor,source,exclude_source) and thelimit/offsetpagination push down into a single SQL statement so the database does the filtering before paginating. Earlier versions of this endpoint filteredsource/exclude_sourcein Python after materializing every row that matchedvendor, which madelimitdescribe the post-fetch slice rather than the actual page size.Results are ordered by
slugso pagination is deterministic across requests.- Parameters:
vendor – Case-insensitive exact vendor match. None disables.
source – Include only rows whose
sourcescontains this token.exclude_source – Drop rows whose
sourcescontains this token.limit – Maximum rows to return. Default 100, max 1000.
offset – Number of filtered rows to skip before returning. Default 0.
- Query Parameters:
vendor ({'null', 'string'})
source ({'null', 'string'})
exclude_source ({'null', 'string'})
limit (integer)
offset (integer)
Example request:
GET /apps HTTP/1.1 Host: example.com
- Status Codes:
200 OK –
Successful Response
Example response:
HTTP/1.1 200 OK Content-Type: application/json [ { "slug": "string", "bundle_id": "string", "name": "string", "vendor": "string", "current_version": "string", "download_url": "https://example.com", "install_method": "dmg", "expected_team_id": "string", "sha256": "string", "sources": [ "string" ] } ]
Validation Error
Example response:
HTTP/1.1 422 Unprocessable Entity Content-Type: application/json { "detail": [ { "loc": [ "string", 1 ], "msg": "string", "type": "string", "input": {}, "ctx": {} } ] }
- GET /apps/{slug}¶
Get App
- Parameters:
slug (string)
Example request:
GET /apps/{slug} HTTP/1.1 Host: example.com
- Status Codes:
200 OK –
Successful Response
Example response:
HTTP/1.1 200 OK Content-Type: application/json { "slug": "string", "bundle_id": "string", "name": "string", "vendor": "string", "current_version": "string", "download_url": "https://example.com", "install_method": "dmg", "expected_team_id": "string", "sha256": "string", "sources": [ "string" ] }
Validation Error
Example response:
HTTP/1.1 422 Unprocessable Entity Content-Type: application/json { "detail": [ { "loc": [ "string", 1 ], "msg": "string", "type": "string", "input": {}, "ctx": {} } ] }
- GET /apps/{slug}/sources¶
Get App Sources
- Parameters:
slug (string)
Example request:
GET /apps/{slug}/sources HTTP/1.1 Host: example.com
- Status Codes:
200 OK –
Successful Response
Example response:
HTTP/1.1 200 OK Content-Type: application/json { "installomator": { "label_name": "string", "label_url": "https://example.com", "raw": {} }, "homebrew_cask": { "token": "string", "cask_json": {} }, "autopkg": { "recipes": [ { "identifier": "string", "name": "string", "shortname": "string", "repo": "string", "path": "string", "parent_identifier": "string", "inferred_type": "string", "recipe_url": "https://example.com" } ] }, "jamf_app_installer": { "title": "string", "source": "string", "host": "string", "bundle_id": "string", "version": "string", "jamf_id": "string", "download_url": "string", "architecture": "string" } }
Validation Error
Example response:
HTTP/1.1 422 Unprocessable Entity Content-Type: application/json { "detail": [ { "loc": [ "string", 1 ], "msg": "string", "type": "string", "input": {}, "ctx": {} } ] }
- POST /apps/{slug}/generate-label¶
Generate Label
Generate an Installomator label for
slug.Projects the app’s Homebrew Cask + Installomator source payloads into an Installomator label fragment that consumers can drop into their Installomator deployments. Returns the label plus provenance metadata (which sources contributed) and any warnings about fields that couldn’t be resolved (most commonly
expectedTeamIDfor Cask-only apps).- Parameters:
slug (string) – URL-friendly app identifier.
session (sqlalchemy.ext.asyncio.AsyncSession) – Async SQLAlchemy session (injected).
slug
- Raises HTTPException:
404 if
slugdoesn’t exist; 422 if the app has no source detail attached (rare — usually a leftover seed record).- Return:
The generated label content + metadata.
- Rtype:
patcher_api.schemas.labels.GeneratedLabel- Status Codes:
200 OK –
Successful Response
Example response:
HTTP/1.1 200 OK Content-Type: application/json { "label_name": "string", "content": {}, "sources_used": [ "string" ], "warnings": [ "string" ] }
Validation Error
Example response:
HTTP/1.1 422 Unprocessable Entity Content-Type: application/json { "detail": [ { "loc": [ "string", 1 ], "msg": "string", "type": "string", "input": {}, "ctx": {} } ] }
- GET /stats¶
Get Stats
Example request:
GET /stats HTTP/1.1 Host: example.com
- Status Codes:
200 OK –
Successful Response
Example response:
HTTP/1.1 200 OK Content-Type: application/json { "total_apps": 1, "sources": { "installomator": 1, "homebrew_cask": 1, "jamf_app_installer": 1, "autopkg": 1 }, "last_refresh": "2026-07-03T16:17:27.937909", "catalog_version": "string" }
Errors¶
Status |
Meaning |
|---|---|
|
OK; response body is the resource. |
|
Not Modified; client’s |
|
Slug doesn’t exist in the catalog. |
|
Request validated but couldn’t be completed (e.g. app has no source detail). |
All error responses follow FastAPI’s standard {"detail": "..."} shape.
OpenAPI schema¶
The full OpenAPI 3.1 schema, including all parameter constraints and exact response shapes, is served at:
https://api.patcherctl.dev/openapi.json
Swagger UI (interactive) is at /docs. ReDoc is at /redoc. Admin-scoped endpoints are deliberately excluded from the schema and not documented publicly.