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:

PatcherAPIClient

For Python consumers. Async client wrapping every public endpoint, returning typed Pydantic models. Lives in the patcher package: from patcher import PatcherAPIClient. See examples.

Patcher API Client
REST

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.

Example response headers¶
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 the limit/offset pagination push down into a single SQL statement so the database does the filtering before paginating. Earlier versions of this endpoint filtered source/exclude_source in Python after materializing every row that matched vendor, which made limit describe the post-fetch slice rather than the actual page size.

Results are ordered by slug so pagination is deterministic across requests.

Parameters:
  • vendor – Case-insensitive exact vendor match. None disables.

  • source – Include only rows whose sources contains this token.

  • exclude_source – Drop rows whose sources contains 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"
            ]
        }
    ]
    

  • 422 Unprocessable Entity –

    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"
        ]
    }
    

  • 422 Unprocessable Entity –

    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"
        }
    }
    

  • 422 Unprocessable Entity –

    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 expectedTeamID for Cask-only apps).

Parameters:
  • slug (string) – URL-friendly app identifier.

  • session (sqlalchemy.ext.asyncio.AsyncSession) – Async SQLAlchemy session (injected).

  • slug

Raises HTTPException:

404 if slug doesn’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"
        ]
    }
    

  • 422 Unprocessable Entity –

    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

200

OK; response body is the resource.

304

Not Modified; client’s If-None-Match matched the current ETag. No body.

404

Slug doesn’t exist in the catalog.

422

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.