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
| Area | Before | After |
|---|---|---|
| Host | A product-specific host | A regional gateway host |
| Request context | Implied by the hostname, or a path or token value | An X-Environment-Id or X-Tenant-Id request header |
| Credentials from | The product's own console | An integration in Jamf Account |
| Authentication | Varies by product | OAuth 2.0 client credentials |
| Permissions | Varies by product | Capability 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 level | Header | Reaches | Choose it when |
|---|---|---|---|
| Platform environment | X-Environment-Id | Platform APIs, plus the product APIs of the tenants inside that environment | Almost all integrations, including anything touching platform APIs or Terraform |
| Tenant | X-Tenant-Id | Product APIs for the specific instances you select | Your 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 from | What to do |
|---|---|
| Jamf Pro API or Classic API | Change host, move context into a header, switch auth, update permission strings |
| Jamf Protect API | Change 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/buildingsAfter
Requests route through a regional gateway, naming the API you're targeting:
GET https://{region}.api.jamfcloud.com/pro/v1/buildings| Component | Description | Values |
|---|---|---|
{region} | The region your instance is hosted in | us, eu, apac |
pro | The API you're targeting | pro 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/buildingsFor 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 API | Platform API Gateway | |
|---|---|---|
| Token endpoint | https://yourserver.jamfcloud.com/api/v1/oauth/token | https://{region}.api.jamfcloud.com/auth/token |
| Grant type | client_credentials | client_credentials |
| Credentials created in | Jamf Pro | Jamf Account |
See Request an access token for the call.
What to remove
| Endpoint | Purpose | What to do instead |
|---|---|---|
POST /v1/auth/token | Bearer token via basic auth | Use the gateway token endpoint |
POST /v1/auth/keep-alive | Refresh a bearer token | Request a new token when the current one expires |
POST /v1/auth/invalidate-token | Revoke a bearer token | Tokens expire on their own after 900 seconds |
GET /auth/current | Current auth session info | Unavailable on the gateway |
POST /v1/oauth/token | Instance-level OAuth token | Use 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 privilege | Platform API capability |
|---|---|
Read Computer Inventory | devices:read |
Update Mobile Devices | devices: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-rolesand/v1/api-role-privilegesendpoints 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 omitsdateCompleted,commandError, andprofileIdMdmCommandType:PROFILE_LIST,INSTALLED_APPLICATION_LIST, andPROVISIONING_PROFILE_LISTare absentComputerPrestageV3: Platform Single Sign-on fields such asauthUrlare absentEnrollmentSettingsV4:personalDeviceProfileEnrollmentTypeis deprecated in the Jamf Pro API and remains an active enum on the gatewayUserAccount:accountTypeis absentPackage:sha3512is absentSiteObject: the gateway addsAndroid Deviceto 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
| Before | After | |
|---|---|---|
| Endpoint | https://<tenant>.protect.jamfcloud.com/graphql | https://{region}.api.jamfcloud.com/protect/graphql |
| Credentials from | An API Client in the Jamf Protect console | An integration in Jamf Account |
| Token request | POST /token with a JSON body of client_id and password | POST /auth/token, form-encoded, grant_type=client_credentials |
| Auth header | The raw token, with no prefix | Authorization: Bearer <token> |
| Request context | Encoded in the hostname | An X-Environment-Id or X-Tenant-Id header |
| Permissions | Roles assigned in the Protect console | Capability 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
Bearerprefix is new. The Protect API accepted the raw token with no prefix. The gateway requiresAuthorization: Bearer <token>. A token sent without the prefix returns401.
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
- Getting started: create an integration and make your first call
- Platform API fundamentals: auth, scope levels, regions, permissions, pagination, and errors
- Jamf Pro permissions map: privilege names to capability equivalents