Jamf Pro
Building DDM Declarations, Part 3: Deployment via the Jamf Pro API
Explains the supported Jamf Pro path for custom DDM declarations: deploy with Blueprints, authenticate to the API, and verify DSS and DDM status data.
Part 1 covered the DDM JSON schema. Part 2 showed how to build DDM declarations with PowerShell objects instead of raw string manipulation.
The natural next question is: how do those declarations get into Jamf Pro?
This is where precision matters. It is easy to assume there must be a simple public endpoint such as POST /api/v2/declarations/upload followed by POST /api/v2/declarations/activate. That shape would be convenient, but it is not the safe assumption to build production automation around.
In current Jamf Pro workflows, custom declarative configurations are deployed through Blueprints. Jamf Pro creates the declaration objects, scopes them through the Blueprint workflow, queues the Declarative Management command, and causes the device to sync the assigned declarations. The Jamf Pro API is then used to authenticate, inspect declaration storage, optionally force a device DDM sync, and verify DDM status data from managed devices.
That distinction is the center of this article: use the API where Jamf supports it, and do not script invented declaration upload endpoints.
Prerequisites and Scope
Before planning a Blueprint-based custom declaration workflow, confirm that the Jamf Pro environment supports Blueprints.
At a minimum, verify:
- Jamf Pro is version 11.15.0 or later.
- The tenant is Jamf Standard Cloud-hosted or Jamf Premium Cloud-hosted.
- OIDC-based SSO is configured through Jamf Account.
- The Jamf Pro user has the required Blueprint, Cloud Services Connection, Jamf Pro URL, and SSO settings privileges.
- Any required Jamf-hosted Blueprint domains are allowed through network controls.
Blueprints are not a general on-premises Jamf Pro feature. If the environment cannot use Blueprints, the supported custom declaration path described in this article does not apply as written.
Also separate two kinds of authentication. The Jamf Pro API authentication shown later in this article does not use SSO. API scripts authenticate to Jamf Pro using API-supported authentication and bearer tokens. Blueprint access in the Jamf Pro interface requires OIDC-based SSO through Jamf Account.
The Supported Jamf Pro Mental Model
For Windows administrators, think of the DDM declaration JSON as the policy artifact and Jamf Pro Blueprints as the deployment wrapper.
A Group Policy setting is not useful until it is linked to scope. A Configuration Manager configuration item is not useful until it is deployed to a collection. An Intune settings catalog policy is not useful until it is assigned. The same principle applies here.
The declaration JSON defines the desired state. Jamf Pro provides the orchestration layer that scopes and delivers that desired state to managed devices.
The supported custom declaration flow is:
- Build or validate the declaration JSON.
- Add the custom declaration to a Jamf Pro Blueprint.
- Scope and deploy the Blueprint.
- Allow Jamf Pro to create and distribute the declaration objects.
- Use the Jamf Pro API to verify stored declaration data and device DDM status.
This does not mean API automation is irrelevant. It means the API should be used for the supported parts of the workflow instead of forcing unsupported assumptions into the deployment path.
Custom Declarations in Blueprints
In the Jamf Pro Blueprint workflow, custom declarations are added as Blueprint components.
The important fields are:
Kind defines whether the item is a Configuration or an Asset.
Channel defines whether the declaration applies at the System or User channel.
Type is the Apple declaration type, such as com.apple.configuration.passcode.settings.
Payload is the JSON payload for that declaration type.
That last point is important. In the Jamf Blueprint form, you are not pasting the entire local development bundle from Part 2. You are entering the declaration type and the declaration payload into the vendor workflow.
For example, the passcode configuration from Part 2 has this declaration shape:
{
"Type": "com.apple.configuration.passcode.settings",
"Identifier": "com.franca.tech.config.passcode-baseline.v1",
"ServerToken": "passcode-baseline-2026-05-28-v1",
"Payload": {
"RequirePasscode": true,
"MinimumLength": 8,
"RequireComplexPasscode": true,
"MaximumFailedAttempts": 10,
"MaximumGracePeriodInMinutes": 5
}
}In the Blueprint custom declaration workflow, the declaration type is entered as:
com.apple.configuration.passcode.settingsand the payload JSON is the object inside Payload:
{
"RequirePasscode": true,
"MinimumLength": 8,
"RequireComplexPasscode": true,
"MaximumFailedAttempts": 10,
"MaximumGracePeriodInMinutes": 5
}The distinction matters because Apple’s declaration schema and Jamf’s Blueprint UI are not the same layer. Apple defines the declaration structure. Jamf provides a management workflow that stores, scopes, and deploys those declarations.
API Endpoints Worth Automating
The Jamf Pro API remains useful in this workflow. The API automation should focus on supported endpoints.
Authentication uses:
POST /api/v1/auth/tokenToken refresh uses:
POST /api/v1/auth/keep-aliveStored declaration retrieval uses:
GET /api/v1/dss-declarations/{declarationId}Device DDM status inspection uses:
GET /api/v1/ddm/{clientManagementId}/status-itemsA single status item can also be retrieved by key:
GET /api/v1/ddm/{clientManagementId}/status-items/{key}A DDM sync can be forced for a device with:
POST /api/v1/ddm/{clientManagementId}/syncThese endpoints support a practical post-deployment validation workflow. After a Blueprint is deployed, the API can verify that the device has received and evaluated declarations. This is the equivalent of checking policy result data after GPO processing or checking compliance state after Configuration Manager baseline evaluation.
The DSS declaration endpoint is a readback endpoint. It retrieves a stored declaration by declaration ID. It is not the same thing as an upload-and-activate deployment workflow.
What Not to Script
Do not hardcode unsupported endpoints such as:
POST /api/v2/declarations/upload
POST /api/v2/declarations/activateDo not assume a direct upload-and-activate workflow exists unless your Jamf Pro tenant’s live API documentation exposes it and Jamf documents the required privileges, request body, response body, and deployment behavior.
This is not a philosophical objection. It is a production engineering rule. Endpoint names that seem plausible are not contracts. A script built around a guessed endpoint is not automation; it is technical debt waiting for a failure window.
API Token Lifetime
Jamf Pro API bearer tokens expire after 20 minutes by default. A script that authenticates, immediately checks status, and exits will usually not care. A verification script that waits for a device to check in, sleeps between retries, or runs as part of a longer pipeline can exceed that window.
For long-running workflows, call:
POST /api/v1/auth/keep-alivewith the current bearer token before it expires. Jamf returns a new token and invalidates the previous token. Any script that uses keep-alive must update its authorization header after the refresh.
The script below includes a helper function for refreshing the bearer token even though the basic verification flow may not need it every time.
PowerShell: Authenticate and Verify DDM State
The following script demonstrates the supported API side of the workflow:
- Authenticate to Jamf Pro.
- Optionally retrieve a stored declaration by declaration ID.
- Optionally force a DDM sync for the device.
- Retrieve the latest DDM status items for a device.
- Handle HTTP failures with useful diagnostics.
- Revoke the API token when finished.
The script assumes the Blueprint deployment has already occurred through the supported Jamf Pro workflow.
<#
.SYNOPSIS
Verifies Jamf Pro DDM declaration and status data after Blueprint deployment.
.DESCRIPTION
This script does not attempt to upload or activate custom declarations through
unsupported endpoints. It authenticates to Jamf Pro, optionally retrieves a
stored declaration from DSS, optionally forces a DDM sync, and retrieves DDM
status items for a managed device.
.REQUIREMENTS
PowerShell 7+
Jamf Pro API account or API client with the following privileges:
Read Computers
Read Mobile Devices
Send Declarative Management Command (required only when -ForceDdmSync is used)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$JamfProUrl,
[Parameter(Mandatory)]
[string]$Username,
[Parameter(Mandatory)]
[securestring]$Password,
[Parameter(Mandatory)]
[string]$ClientManagementId,
[Parameter()]
[string]$DeclarationId,
[Parameter()]
[switch]$ForceDdmSync
)
function New-JamfBasicAuthHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Username,
[Parameter(Mandatory)]
[securestring]$Password
)
$PlainPassword = [System.Net.NetworkCredential]::new("", $Password).Password
$Bytes = [System.Text.Encoding]::UTF8.GetBytes("${Username}:${PlainPassword}")
$Encoded = [System.Convert]::ToBase64String($Bytes)
return @{
Authorization = "Basic $Encoded"
Accept = "application/json"
}
}
function Invoke-JamfApiRequest {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Uri,
[Parameter(Mandatory)]
[ValidateSet("GET", "POST", "PUT", "PATCH", "DELETE")]
[string]$Method,
[Parameter(Mandatory)]
[hashtable]$Headers,
[Parameter()]
[string]$Body
)
try {
$Parameters = @{
Uri = $Uri
Method = $Method
Headers = $Headers
ErrorAction = "Stop"
}
if ($PSBoundParameters.ContainsKey("Body")) {
$Parameters.Body = $Body
$Parameters.ContentType = "application/json"
}
return Invoke-RestMethod @Parameters
}
catch {
$StatusCode = $null
$ResponseBody = $null
if ($_.Exception.Response) {
try {
$StatusCode = [int]$_.Exception.Response.StatusCode
}
catch {
$StatusCode = "Unknown"
}
}
if ($_.ErrorDetails.Message) {
$ResponseBody = $_.ErrorDetails.Message
}
$Message = @(
"Jamf Pro API request failed."
"Method: $Method"
"URI: $Uri"
"HTTP Status: $StatusCode"
"Response Body: $ResponseBody"
"Exception: $($_.Exception.Message)"
) -join [Environment]::NewLine
throw $Message
}
}
function New-JamfBearerHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Token
)
return @{
Authorization = "Bearer $Token"
Accept = "application/json"
}
}
function Update-JamfBearerToken {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$JamfProUrl,
[Parameter(Mandatory)]
[hashtable]$BearerHeaders
)
$KeepAliveResponse = Invoke-JamfApiRequest `
-Uri "$JamfProUrl/api/v1/auth/keep-alive" `
-Method POST `
-Headers $BearerHeaders
if (-not $KeepAliveResponse.token) {
throw "Jamf Pro keep-alive response did not include a replacement token."
}
return New-JamfBearerHeader -Token $KeepAliveResponse.token
}
# Normalize URL: strip any trailing slash to prevent double-slash in constructed URIs.
$JamfProUrl = $JamfProUrl.TrimEnd("/")
# Step 1: Authenticate.
Write-Host "Authenticating to Jamf Pro..."
$AuthHeaders = New-JamfBasicAuthHeader -Username $Username -Password $Password
$AuthResponse = Invoke-JamfApiRequest `
-Uri "$JamfProUrl/api/v1/auth/token" `
-Method POST `
-Headers $AuthHeaders
if (-not $AuthResponse.token) {
throw "Jamf Pro authentication response did not include a token."
}
$BearerHeaders = New-JamfBearerHeader -Token $AuthResponse.token
try {
# Step 2: Optionally retrieve a stored declaration.
if ($DeclarationId) {
Write-Host "Retrieving declaration from DSS: $DeclarationId"
$Declaration = (Invoke-JamfApiRequest `
-Uri "$JamfProUrl/api/v1/dss-declarations/$DeclarationId" `
-Method GET `
-Headers $BearerHeaders).declarations
Write-Host "Stored declaration retrieved ($($Declaration.Count) item(s)):"
$Declaration | ConvertTo-Json -Depth 10
}
else {
Write-Host "No DeclarationId supplied. Skipping DSS declaration retrieval."
}
# Step 3: Optionally force a DDM sync.
if ($ForceDdmSync) {
Write-Host "Forcing DDM sync for client management ID: $ClientManagementId"
Invoke-JamfApiRequest `
-Uri "$JamfProUrl/api/v1/ddm/$ClientManagementId/sync" `
-Method POST `
-Headers $BearerHeaders | Out-Null
Write-Host "DDM sync command queued."
}
# If your workflow waits here for device check-in, refresh the token before continuing.
# Example:
# Start-Sleep -Seconds 900
# $BearerHeaders = Update-JamfBearerToken -JamfProUrl $JamfProUrl -BearerHeaders $BearerHeaders
# Step 4: Retrieve latest DDM status items for the device.
Write-Host "Retrieving DDM status items for client management ID: $ClientManagementId"
$StatusItems = Invoke-JamfApiRequest `
-Uri "$JamfProUrl/api/v1/ddm/$ClientManagementId/status-items" `
-Method GET `
-Headers $BearerHeaders
if (-not $StatusItems.statusItems -or $StatusItems.statusItems.Count -eq 0) {
Write-Warning "No DDM status items found for client management ID: $ClientManagementId. The device may not have sent a DDM status report yet."
}
else {
Write-Host "DDM status items retrieved:"
$StatusItems | ConvertTo-Json -Depth 10
}
}
finally {
# Step 5: Revoke the token.
Write-Host "Revoking Jamf Pro API token..."
try {
Invoke-JamfApiRequest `
-Uri "$JamfProUrl/api/v1/auth/invalidate-token" `
-Method POST `
-Headers $BearerHeaders | Out-Null
Write-Host "Token revoked."
}
catch {
Write-Warning "Token revocation failed. Review Jamf Pro token settings and logs."
Write-Warning $_.Exception.Message
}
}Reading the Status Data
The DDM status endpoint is where the architecture becomes visible.
A deployed Blueprint may produce one or more declaration objects. The device evaluates those declarations and reports state. The status response is the place to check whether a declaration is active, valid, invalid, or unknown.
For administrators coming from Windows, this is the conceptual equivalent of reading Resultant Set of Policy, Configuration Manager compliance state, or Intune device configuration status. The declaration exists as an intended state. The device reports what happened when it evaluated that intended state.
A useful troubleshooting sequence is:
- Confirm the device is in the Blueprint scope.
- Confirm Jamf Pro queued the Declarative Management command.
- Retrieve the latest DDM status items for the device.
- Look for declaration status entries.
- Check whether each declaration is active.
- Check whether each declaration is valid, invalid, or unknown.
- Investigate invalid declarations as schema, payload, hosting, or applicability problems.
valid: invalid generally means the device evaluated the declaration and found a problem. valid: unknown may be expected in some predicate-driven workflows where a declaration does not apply to the current device state. Context matters.
Error Handling Strategy
For production automation, error handling should treat each HTTP response class differently.
400 Bad Request usually means the request itself is malformed. Check parameter values and URI construction.
401 Unauthorized usually means authentication failed or the token is missing, expired, or invalid.
403 Forbidden usually means the API account authenticated successfully but lacks the required Jamf Pro privileges.
404 Not Found usually means the client management ID or declaration ID is wrong, stale, or unavailable in that tenant.
409 Conflict usually means the requested operation conflicts with existing state.
500 Server Error means the request reached Jamf Pro but Jamf Pro could not complete the operation. Capture the response body and review Jamf Pro service health and logs.
The script above captures the method, URI, HTTP status, response body, and exception message so the failure can be diagnosed without rerunning the command in a debugger.
Where the JSON from Part 2 Fits
The JSON from Part 2 remains valuable. It gives you a repeatable, reviewable way to generate the declaration content that will be entered into the supported Jamf workflow.
For a custom declaration Blueprint, the important transformation is this:
The PowerShell object may represent the full declaration for development and source control.
The Jamf Blueprint custom declaration form expects the declaration kind, channel, type, and payload in the fields Jamf exposes.
That means your automation pipeline can still generate and validate the payload JSON, commit it to source control, and subject it to review. The final deployment step must align with the Jamf Pro workflow available in your tenant.
The Practical Boundary
Use PowerShell to build and validate DDM JSON as a structured artifact. Use Jamf Pro Blueprints to deploy custom declarations when the tenant meets the Blueprint requirements. Use the Jamf Pro API for authentication, declaration readback, DDM sync, and device status verification. Avoid guessed upload or activation endpoints unless Jamf documents them in the live API reference for the tenant you are automating.
That boundary keeps the workflow accurate, supportable, and resilient. It also preserves the main benefit of the series: declarations are structured, testable artifacts that can be generated programmatically, reviewed like code, deployed through the supported management workflow, and verified through the platform after deployment.