Move an existing integration to the Platform API Gateway

What to change in an integration built against the Jamf Pro, Classic, or Jamf Protect APIs

The Platform API Gateway changes four things for an existing integration: where you send requests, how you authenticate, how you pass tenant context, and how permissions are named. Request bodies, response schemas, pagination, and filtering stay as they are.

Those four changes work the same way for every product, and Platform API fundamentals documents them in full. This guide covers what is specific to the API you are moving from.

What changes for everyone

AreaBeforeAfter
HostA product-specific hostA regional gateway host
Request contextImplied by the hostname, or a path or token valueAn X-Environment-Id or X-Tenant-Id request header
Credentials fromThe product's own consoleAn integration in Jamf Account
AuthenticationVaries by productOAuth 2.0 client credentials
PermissionsVaries by productCapability permissions, {capability}:{action}

Two behaviors differ from every product's existing API, so check them before you start:

  • Tokens last 900 seconds and there is no keep-alive or refresh token. Request a new one with the same client credentials.
  • Tokens are region-locked. Request yours from the same host you send requests to.

Choose a scope level before you start

Your integration is scoped when you create it in Jamf Account, and that choice decides which header you send. Most integrations should be scoped at the Platform environment level.

Scope levelHeaderReachesChoose it when
Platform environmentX-Environment-IdPlatform APIs, plus the product APIs of the tenants inside that environmentAlmost all integrations, including anything touching platform APIs or Terraform
TenantX-Tenant-IdProduct APIs for the specific instances you selectYour integration is deliberately confined to a single product instance

A platform environment is a customer's group of product instances: one MDM, meaning Jamf Pro or Jamf School, plus up to one tenant of each other Jamf product. An environment-scoped request names the environment and the gateway resolves it to the right product tenant.

The reason to prefer environment scope is reach. Platform APIs such as Blueprints and Compliance Benchmarks are available at environment scope only, because they depend on the way tenants are interconnected inside an environment, and the Jamf Platform Terraform provider works with an environment ID only. A tenant-scoped credential cannot reach either.

⚠️

Scope to a platform environment wherever you can. We encourage you to scope your integrations to platform environments, and use tenant-level scoping only if your use case requires it. Platform environment-scoped integrations also get access to the Jamf Platform APIs and the Jamf Platform Terraform provider.

📘

Send one header or the other, matching the scope level your integration was created with. A header that does not match your credential's scope returns 403.

Find both IDs in the Integration details panel in Jamf Account by clicking the relevant pill. If you do not see a platform environment to select, your tenants may not be grouped into one yet. Environments can be created and edited in the platform environments UI in Jamf Account.

Which product are you moving from?

Coming fromWhat to do
Jamf Pro API or Classic APIChange host, move context into a header, switch auth, update permission strings
Jamf Protect APIChange host, add the Bearer prefix and the context header, swap console roles for capabilities

Jamf Pro and the Classic API

1. Update your base URL

Before

Requests go directly to your Jamf Pro instance:

GET https://yourserver.jamfcloud.com/api/v1/buildings

After

Requests route through a regional gateway, naming the API you're targeting:

GET https://{region}.api.jamfcloud.com/pro/v1/buildings
ComponentDescriptionValues
{region}The region your instance is hosted inus, eu, apac
proThe API you're targetingpro for the Jamf Pro API, proclassic for the Classic API

Every endpoint follows this pattern:

/{api}/{version}/{resource-path}
📘

Version prefixes are preserved. Your /v1/ and /v2/ paths stay as they are, and no version numbers changed at GA. Only the host and the leading {api} segment are new.

# Before
curl https://yourserver.jamfcloud.com/api/v1/buildings

# After
curl https://us.api.jamfcloud.com/pro/v1/buildings \
  -H "X-Environment-Id: 4b91d0e2-7c33-4f18-9a52-1d6e8f0b3c47"

2. Move request context into a header

Your Jamf Pro hostname used to identify your tenant. On the gateway, one host serves every customer in a region, so you name your context explicitly. Send the header that matches your integration's scope level:

X-Environment-Id: <your-environment-UUID>
X-Tenant-Id: <your-tenant-UUID>

An environment-scoped request is the recommended form. The gateway resolves the environment to your Jamf Pro tenant, so the same credential also reaches Jamf Protect and the platform APIs without a second integration. A tenant-scoped request names the Jamf Pro instance directly and reaches that instance only.

Copy either UUID from the Integration details panel in Jamf Account by clicking the relevant pill.

⚠️

If you built against the public beta, this replaces the path form. The beta put tenant and environment IDs in the URL path as /tenant/{tenantId}/. Production accepts the header form only.

Updating your code

If your integration builds URLs from a base URL variable, swap the base URL and move the context into a default header:

# Before
BASE_URL = "https://yourserver.jamfcloud.com/api"
HEADERS = {"Authorization": f"Bearer {token}"}

def get_url(path):
    return f"{BASE_URL}{path}"

# get_url("/v1/buildings") -> https://yourserver.jamfcloud.com/api/v1/buildings
# After
API = "pro"          # "pro" or "proclassic"
REGION = "us"        # "us", "eu", or "apac"
ENVIRONMENT_ID = "4b91d0e2-7c33-4f18-9a52-1d6e8f0b3c47"

BASE_URL = f"https://{REGION}.api.jamfcloud.com/{API}"
HEADERS = {
    "Authorization": f"Bearer {token}",
    "X-Environment-Id": ENVIRONMENT_ID,
}

def get_url(path):
    return f"{BASE_URL}{path}"

# get_url("/v1/buildings") -> https://us.api.jamfcloud.com/pro/v1/buildings

For a tenant-scoped integration, the only difference is the header:

TENANT_ID = "e77c1408-10c8-4007-b177-abc9157fbcaa"

HEADERS = {
    "Authorization": f"Bearer {token}",
    "X-Tenant-Id": TENANT_ID,
}

Your path-building logic stays as it was either way. Set the header once on your session or client and every call includes it.

💡

Using jamf-cli, the Terraform provider, or the Go SDK? All three moved from path rewrites to headers for GA. Update to a GA-compatible version rather than patching the URL yourself.

3. Switch to OAuth 2.0 client credentials

Before

The Jamf Pro API supports three authentication methods:

  • Basic authentication: username and password, used to obtain a bearer token from /v1/auth/token
  • Bearer token: a JWT obtained from basic auth, kept alive via /v1/auth/keep-alive
  • API client credentials: OAuth 2.0 client credentials, with tokens from /v1/oauth/token

After

Jamf Pro APIPlatform API Gateway
Token endpointhttps://yourserver.jamfcloud.com/api/v1/oauth/tokenhttps://{region}.api.jamfcloud.com/auth/token
Grant typeclient_credentialsclient_credentials
Credentials created inJamf ProJamf Account

See Request an access token for the call.

What to remove

EndpointPurposeWhat to do instead
POST /v1/auth/tokenBearer token via basic authUse the gateway token endpoint
POST /v1/auth/keep-aliveRefresh a bearer tokenRequest a new token when the current one expires
POST /v1/auth/invalidate-tokenRevoke a bearer tokenTokens expire on their own after 900 seconds
GET /auth/currentCurrent auth session infoUnavailable on the gateway
POST /v1/oauth/tokenInstance-level OAuth tokenUse the gateway token endpoint
⚠️

Basic auth callers need a new credential. Create an integration in Jamf Account with the capabilities your workflow needs before you migrate. The gateway accepts client credentials only.

4. Update your permission references

Permissions move from human-readable privilege names to capability permissions.

Jamf Pro privilegePlatform API capability
Read Computer Inventorydevices:read
Update Mobile Devicesdevices:update

Capabilities span products, so one devices:read covers device inventory across Jamf Pro, Jamf Protect, and the platform Devices API. See Capability permissions for the format and the action list.

Update your code if you:

  • Check permissions in application logic. Hardcoded privilege strings need updating.
  • Grant least privilege deliberately. Review your capability list against what your integration calls.
📘

API roles and API clients are managed in Jamf Account now, so the /v1/api-roles and /v1/api-role-privileges endpoints are unavailable through the gateway. Code that created or updated roles programmatically moves to Jamf Account.

5. Check endpoint and schema availability

Removed by design

Authentication endpoints. The gateway token endpoint replaces all of them: /v1/auth/token, /v1/auth/keep-alive, /v1/auth/invalidate-token, /auth/current, /auth/keepAlive, /auth/invalidateToken, /v1/auth, /v1/oauth/token, /v1/oauth2/session-tokens.

API clients, API roles, and peripherals. Credential management moved to Jamf Account, and the peripherals endpoints were retired.

Unavailable through the gateway

Local Admin Password (LAPS). The /v2/local-admin-password/ set is unavailable. Keep those calls on the Jamf Pro API for now.

Remote administration (TeamViewer). The /preview/remote-administration-configurations preview endpoints are unavailable.

Schema differences

Most shared schemas are identical between the two specifications. Differences worth checking if you use strict deserialization:

  • MdmCommand: the gateway version omits dateCompleted, commandError, and profileId
  • MdmCommandType: PROFILE_LIST, INSTALLED_APPLICATION_LIST, and PROVISIONING_PROFILE_LIST are absent
  • ComputerPrestageV3: Platform Single Sign-on fields such as authUrl are absent
  • EnrollmentSettingsV4: personalDeviceProfileEnrollmentType is deprecated in the Jamf Pro API and remains an active enum on the gateway
  • UserAccount: accountType is absent
  • Package: sha3512 is absent
  • SiteObject: the gateway adds Android Device to the object type enum

Others with differences: ApplicationAttributes, ApplicationConfiguration, Attributes, CloudLdapServerRequest, EnrollmentProcessTextObject, InventoryListMobileDevice, JamfProInformationV2, JamfProtectPlan, MdmClientType, MdmCommandRequest, MobileDeviceGeneral, MobileDeviceLostModeLocation, MobileDevicePrestageV3, MobileDeviceSecurity, NotificationType, OidcLoginDispatchResponseV2, PlanSearchResults, ProtectSettingsResponse, ProtectUpdatableSettingsRequest, SettingsCommand, SharedDeviceConfiguration


Jamf Protect

The GraphQL schema is unchanged. Every query, mutation, and type works as before, so your operations and response handling carry over without edits.

What changes

BeforeAfter
Endpointhttps://<tenant>.protect.jamfcloud.com/graphqlhttps://{region}.api.jamfcloud.com/protect/graphql
Credentials fromAn API Client in the Jamf Protect consoleAn integration in Jamf Account
Token requestPOST /token with a JSON body of client_id and passwordPOST /auth/token, form-encoded, grant_type=client_credentials
Auth headerThe raw token, with no prefixAuthorization: Bearer <token>
Request contextEncoded in the hostnameAn X-Environment-Id or X-Tenant-Id header
PermissionsRoles assigned in the Protect consoleCapability permissions
💡

Environment scope saves you a step here. Scope your integration to the platform environment and the gateway resolves it to the right Jamf Protect tenant, so you never name a Protect tenant yourself.

⚠️

The Bearer prefix is new. The Protect API accepted the raw token with no prefix. The gateway requires Authorization: Bearer <token>. A token sent without the prefix returns 401.

Updating your code

# Before
base = "https://your-tenant.protect.jamfcloud.com"
token = requests.post(
    f"{base}/token",
    json={"client_id": CLIENT_ID, "password": PASSWORD},
).json()["access_token"]

resp = requests.post(
    f"{base}/graphql",
    json={"query": query},
    headers={"Authorization": token},        # raw token
)
# After
base = f"https://{REGION}.api.jamfcloud.com"
token = requests.post(
    f"{base}/auth/token",
    data={
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    },
).json()["access_token"]

resp = requests.post(
    f"{base}/protect/graphql",
    json={"query": query},                   # unchanged
    headers={
        "Authorization": f"Bearer {token}",  # Bearer prefix required
        "X-Environment-Id": ENVIRONMENT_ID,  # or "X-Tenant-Id": TENANT_ID
    },
)

Permissions

Scope your integration to the platform environment level, or to the tenant level if you want it confined to one Jamf Protect tenant. Because GraphQL has one endpoint and one HTTP verb, permissions map to operation names rather than paths. The Jamf Protect API overview lists which capability each operation requires.

📘

Protect authorization requires an integration that uses the client credentials flow. Tokens from the authorization-code flow cannot authorize Protect operations.


Related articles