Repository navigation
Jamf Security Cloud Commands
Jamf Security Cloud commands live under the security namespace (alias sec). For shared CLI patterns (scaffold, --set, dry-run, destructive confirmation), see CLI Patterns.
jamf-cli security <resource> <action> [flags]
Two APIs serve Jamf Security Cloud. They take different credentials, and one profile can carry both.
| Half | Marker in --help
|
Resources | Served on | Credentials from |
|---|---|---|---|---|
| Radar API | (Security Cloud · Radar API) |
risk, device-lifecycle, stream, status, verification, well-known, jwks (seven, and no others) |
api.wandera.com / sse.jamf.com
|
jamf-cli security setup (three scoped application ID/secret pairs) |
| Platform gateway | (Security Cloud · platform gateway) |
everything else: dns-*, ztna-*, content-categories, device-groups, uem-*, enrollment-activation-profiles
|
https://{region}.api.jamfcloud.com/securitycloud/… |
jamf-cli platform setup (a platform gateway credential) |
Each command names its half in its one-line description, which is also what security <TAB> shows:
jamf-cli security --help
# dns-zones Manage dns-zones (Security Cloud · platform gateway)
# risk Manage risk (Security Cloud · Radar API)The command catalog carries the same fact as an api field (platform-gateway or radar), for scripts:
jamf-cli commands -o json | jq -r '.[] | select(.product=="security") | .command + " " + (.api // "")'
# The gateway-served half
jamf-cli commands -o json | jq -r '.[] | select(.product=="security" and .api=="platform-gateway") | .command'security setup carries no marker: it writes credentials and calls no API.
Each Radar API is its own "Security Integration" under Settings > Security Integrations in the Radar portal, with its own application ID/secret pair: one for Risk, one for Device Lifecycle, one for Shared Signals & Events. The JWT a pair returns is scoped to that API alone.
jamf-cli tracks the three pairs independently, so configure whichever you have access to. A command for an unconfigured API names that API and exits 1:
{
"error": "general",
"exitCode": 1,
"exitCodeName": "general",
"message": "list: Risk API credentials are not configured; run 'jamf-cli security setup'"
}Device Lifecycle and SSE produce the equivalent message for themselves. A profile carrying only the gateway credential runs the gateway-served commands and reports which Radar pair is missing when you reach for a Radar command.
There is no per-tenant URL to configure: the three APIs share one production host, and tenancy travels inside the credentials. Host overrides exist for regional or sandbox hosts; see Configuration & Profiles#Jamf Security Cloud.
The DNS, ZTNA, content-category, device-group, UEM Connect and enrollment resources are served on the Jamf Platform gateway and need a platform gateway credential (an API integration created in Jamf Account) instead of the Radar pairs. Run jamf-cli platform setup, or write the profile by hand:
# Platform environment scope: the level to prefer
jamf-cli config add-profile my-jsc \
--url https://eu.api.jamfcloud.com \
--auth-method platform \
--environment-id <platform-environment-id>
# Tenant scope: the legacy level
jamf-cli config add-profile my-jsc-tenant \
--url https://eu.api.jamfcloud.com \
--auth-method platform \
--tenant-id <security-cloud-tenant-id>A gateway-served command with no platform credentials resolved fails with this command requires platform gateway auth and a config add-profile line. See Platform API GA Migration for regions, scope levels and the GA base URL.
Scope level. Prefer environment scope: one EU environment credential reaches the whole gateway-served Security Cloud surface, with content categories, DNS, ZTNA, UEM Connect and device groups all answering 200. Tenant scope (
X-Tenant-Id) is the legacy level, anddevice-groupshits a gateway fault under it (below). A tenant credential lacking the Security Cloud grants answers403 BAD_PERMISSIONS, which is an answer about grants and not about the level. The two levels are mutually exclusive: supplying both is refused.
security setup and platform setup merge into the named profile, so run both against one name to get a profile that reaches everything:
jamf-cli security setup --profile-name jsc # Radar pairs
jamf-cli platform setup --profile-name jsc # gateway credential + scope
jamf-cli -p jsc security risk list # Radar
jamf-cli -p jsc security dns-zones list # gatewayOn a re-run, a pair you skip is left as it was. To drop a pair, remove the profile (jamf-cli config remove-profile <name>) and run setup again with only what you want.
| Command | Alias |
|---|---|
security |
sec |
device-lifecycle |
lifecycle, dl
|
enrollment-activation-profiles |
eap |
# Radar APIs: interactive wizard, one application ID/secret pair per API
jamf-cli security setup
# Save under a custom profile name (default: "security")
jamf-cli security setup --profile-name radar
# Platform gateway half (default profile name: "platform")
jamf-cli platform setupThe Radar wizard prompts for the Risk, Device Lifecycle, and Shared Signals & Events application ID/secret pairs in turn. Leave an application ID blank to skip that API. Secrets are stored in the system keychain. See Setup Guide#Jamf Security Cloud Quick Start for the full walkthrough.
| Flag | Description |
|---|---|
--profile-name |
Profile name (default: security) |
security setup collects credentials through interactive prompts with hidden input, so it cannot run under --no-input. For CI/CD, use environment variables:
| Variable | API |
|---|---|
JAMFSECURITY_RISK_CLIENT_ID / _SECRET
|
Risk |
JAMFSECURITY_LIFECYCLE_CLIENT_ID / _SECRET
|
Device Lifecycle |
JAMFSECURITY_SSE_CLIENT_ID / _SECRET
|
Shared Signals & Events |
JAMFSECURITY_URL, JAMFSECURITY_SSE_URL
|
Host overrides for a regional or sandbox host. Since v1.32.0 JAMFSECURITY_URL alone moves the Radar host; --url and a profile's url select the gateway only |
Set both halves of a pair or neither: an ID from the environment with the secret coming from a profile is refused. The gateway half reads the ordinary platform variables (JAMF_URL, JAMF_CLIENT_ID, JAMF_CLIENT_SECRET, and either JAMF_ENVIRONMENT_ID or JAMF_TENANT_ID, never both). See Configuration & Profiles#Jamf Security Cloud.
security open opens https://radar.wandera.com. Jamf Security Cloud has no per-tenant URL, tenancy living in the JWT's customer_id claim, so the target is a compiled-in constant. This command carries jamf:no-auth, makes no request, and needs no credentials and no configuration.
# Open Radar
jamf-cli security open
# Print the URL instead of launching a browser
jamf-cli security open --printThe URL is printed rather than launched under any of --print, --no-input, --dry-run, --out-file, --field, --select, or a non-terminal stdout, so jamf-cli security open | pbcopy and a CI job both get the URL instead of a failed launch; read it with -o json or --field url. On the launch path stdout stays empty and the note goes to stderr. $BROWSER wins where it is set, otherwise the platform's own opener.
The other three products resolve a per-tenant URL instead, and pro open also takes a section name. See Jamf Pro Commands.
Every security write command that takes a body, on both halves, accepts the same inputs, and --scaffold prints the template:
jamf-cli security dns-zones create --scaffold > zone.json
# ... edit zone.json ...
jamf-cli security dns-zones create --from-file zone.json
# The body can arrive on stdin instead
jamf-cli security dns-zones create --scaffold | vipe | jamf-cli security dns-zones create
# Overlay individual fields onto the body, or build a small body outright
jamf-cli security dns-zones create --from-file zone.json --set name=corp-internal| Flag | Behaviour |
|---|---|
--from-file <path> |
Request body from a JSON or YAML file. Omit it and the body is read from stdin |
--set key=value |
Repeatable, supports nested.keys; overlays onto the body |
--scaffold |
Print an example body and exit (no auth needed) |
Rules that change what you type:
- The flag is
--from-file, the same name the Jamf Pro, Classic, Protect and School commands use.--fileanswersunknown flag: --filewithhint: did you mean --from-file?and exit 2, in thehintfield of-o jsonas well as on stderr.--filestill names an upload payload on the commands that send one, such aspro packages upload; those take no pipe, because the transport needs a filename and a length. -
Stdin is read when
--from-fileis absent. An empty pipe is not a body, since a CI runner hands every process a stdin that carries nothing. A named--from-filethat is empty is an error. -
--setoverlays onto a--from-fileor piped body. Classic's--setbuilds the whole body and refuses to be combined with a file. -
--setrefuses to overwrite a non-object field:--set ipsec.esp.lifetimeInSec=14400over a body whoseipsecis a string answerscannot set nested key under non-object field "ipsec". -
--setbuilds an object, so it cannot express a request body that is a top-level JSON array.dns-custom-hostname-mappings updateis one; use--from-filefor it. -
A piped body over 10MB is refused, never truncated:
stdin body exceeds 10MB; use --from-file for a larger payload. Use--from-filefor a payload that size. -
--out-file,--select,--compact,--quietand--no-hintswork on every command in the namespace;--selectand--compactnarrow rows for every column format. -
Enum-constrained fields list their values in
--help, underAllowed values:, one line per dotted field path. A[]suffix on the path puts the constraint on each array element:ipsec.esp.encryption[]: 3des, aes128, aes256constrains every element of that array. A scaffold renders an enum as"", so--helpis where the legal set lives. - A scaffold shows one specimen element inside an array of objects, and prefers a spec example over a placeholder. Edit it; do not pipe it through unread.
-
-n, --dry-runis honoured on these writes: the method, resolved path and body go to stderr and nothing is sent. The preview comes before the confirmation prompt, so-nworks on destructive commands without--yes. - An empty list prints
[], nevernull, so it is safe to pipe intojq.
Five gateway resources take apply, the same idempotent verb the pro namespace has: dns-zones, ztna-apps, ztna-gateways, ztna-grouped-gateways and device-groups. (platform ai-policies is the sixth, outside this namespace; see Jamf Platform Commands.)
apply reads a body, takes the name out of it, and looks that name up in the collection. Absent, it creates; present, it updates after a confirmation, which --yes skips.
# Idempotent: run it twice, get one gateway
jamf-cli security ztna-gateways apply --from-file gateway.yaml --yes
# Scaffold, edit, apply with no temp file
jamf-cli security ztna-gateways apply --scaffold | vipe | jamf-cli security ztna-gateways apply --yes
# Preview the branch: the lookup is a read, so -n reports which one runs
jamf-cli security dns-zones apply --from-file zone.yaml --dry-run
# [dry-run] Would create dns-zone "corp-internal"
# [dry-run] POST /securitycloud/v1/dns/zonesFour things to know:
-
The name comes from the body. There is no
--nameonapply. A body with noname, or anamethat is not a non-empty string, is refused before anything is sent. -
Four of the five update with PATCH, so fields you omit keep their current values.
device-groupsupdates with aPUTand replaces the group wholesale, asproandclassicapplydo. Each command's--helpsays which it has. To make a resource match your document, send a complete body;--scaffoldprints one. - A failed lookup takes no branch. Only a genuine "not found" creates; an auth error or a 5xx during the lookup aborts.
-
The lookup and the create are two requests, so two runs racing on the same absent name (a CI retry, or concurrent jobs) can both create one. No spec in this namespace declares a name-uniqueness
409. Serialiseapplyper resource where that matters.
The lookup lists the collection and accumulates matches across every page before deciding, so a name repeated either side of a 100-item page boundary is an ambiguity error. A name matching an item the list returns no ID for is reported as that; device groups are the live case.
apply carries the same jamf:scopes annotation as its sibling verbs, so it appears under scopes in jamf-cli commands -o json and earns the scope-level note the CLI appends to a missing-scope 400.
The Radar half has no apply: those resources are singletons, actions and read-only documents, with no named collection to match a name against.
Read and override the calculated risk status of managed devices. Requires the Risk API credentials.
# List device risk status
jamf-cli security risk list
# Narrow to a single device by its guid
jamf-cli security risk list --guid <device-guid>
# Narrow to a single device by its externalId
jamf-cli security risk list --external-id <external-id>
# Override the calculated risk: scaffold, edit, then apply (deviceIds is a list)
jamf-cli security risk override --scaffold > override.json
# ... edit override.json ...
jamf-cli security risk override --from-file override.json
# Set the scalar fields inline (combine with a body file for deviceIds)
jamf-cli security risk override --set risk=<risk-level> --set source="manual review"| Flag | Description |
|---|---|
--guid |
Return a single device by its guid
|
--external-id |
Return a single device by its externalId
|
{
"deviceIds": [],
"risk": "",
"source": ""
}
deviceIdsis declared upstream as an array with no element schema, so--scaffoldrenders it as[]. Fill it fromrisk list.
Purge devices from Jamf Security Cloud. The most destructive command in the CLI: it requires --yes or an interactive confirmation. Requires the Device Lifecycle API credentials.
# Scaffold, edit (fill externalIds and/or guids), then purge
jamf-cli security device-lifecycle purge --scaffold > purge.json
# ... edit purge.json ...
# Preview first. The preview prints before any confirmation prompt,
# so this works without --yes
jamf-cli security dl purge --from-file purge.json --dry-run
# Execute
jamf-cli security dl purge --from-file purge.json --yes{
"externalIds": [],
"guids": []
}
jamf-clifills the customer ID in from the Device Lifecycle credentials at request time; it is never user-facing. A body that scopes nothing is refused before the preview.
Configure and monitor the Jamf Security Cloud Shared Signals & Events transmitter, an OpenID Shared Signals Framework stream. Requires the Shared Signals & Events API credentials.
# Get the current event stream configuration
jamf-cli security stream get
# Create or replace the event stream configuration
jamf-cli security stream update --from-file stream.json
# Set individual scalar fields
jamf-cli security stream update --set format=<format> --set aud="https://receiver.example.com"
# Print the request-body scaffold
jamf-cli security stream update --scaffold
# Delete the event stream configuration (destructive)
jamf-cli security stream delete --yesThe stream update scaffold:
{
"aud": "",
"delivery": null,
"events_delivered": [],
"events_requested": [],
"events_supported": [],
"format": "",
"iss": "",
"min_verification_interval": 0
}# Get the current stream status
jamf-cli security status get
# Update the stream status
jamf-cli security status update --set status=<status> --set reason="reactivating stream"
# Print the request-body scaffold
jamf-cli security status update --scaffoldThe status update scaffold:
{
"reason": "",
"status": ""
}# Trigger an SSE stream verification event
jamf-cli security verification trigger
# Include a state value echoed back in the verification event
jamf-cli security verification trigger --set state=<opaque-value>
# Print the request-body scaffold
jamf-cli security verification trigger --scaffold# Get the SSE transmitter discovery document (.well-known metadata)
jamf-cli security well-known get
# Get the SSE transmitter's JSON Web Key Set
jamf-cli security jwks getEvery command below needs a platform gateway credential instead of the Radar pairs. An environment-scoped credential reaches all of them.
jamf-cli security dns-zones list
jamf-cli security dns-zones list --sort name:desc
jamf-cli security dns-zones get <id>
jamf-cli security dns-zones get --name corp-internal
jamf-cli security dns-zones create --scaffold > zone.json
jamf-cli security dns-zones create --from-file zone.json
# Partial update: JSON Merge Patch (RFC 7396). Omitted fields are left alone;
# null clears an optional field.
jamf-cli security dns-zones patch <id> --set name=corp-internal-eu
jamf-cli security dns-zones delete <id> --yesThe create / patch scaffold:
{
"domains": [
"example.com"
],
"name": "corp-internal",
"nameServers": [
{
"gatewayId": "gw-eu-01",
"ip": "203.0.113.53"
}
]
}-
nameServers[].ipmust be a public address. The server refuses a private one with422 NAMESERVER_IP_RESTRICTED:10.0.0.53fails,198.51.100.53is accepted. An unknowngatewayIdis422 GATEWAY_NOT_FOUND. The ZTNA rule below runs the other way:ipsec.left.subnets[]must be private. -
listdoes not paginate: the full set comes back in one response, bounded by a per-customer zone cap.--sortacceptsname:asc/name:descand no other field. - Setting a required field to
nullin apatchis a 400. There is no optimistic locking, so concurrent patches are last-writer-wins. - The privileges are
ztna:read/ztna:create/ztna:update/ztna:delete: DNS zones share the ZTNA permission family and have nodns:*one.
jamf-cli security dns-search-domains get
jamf-cli security dns-search-domains update --set suffix=corp.example.com
jamf-cli security dns-search-domains delete --yesScaffold: {"suffix": "corp.example.com"}. update overwrites any existing suffix.
geton a tenant with no search domain renders{}and exits 0. The server answers404 SEARCH_DOMAIN_NOT_SETand the CLI carries that documented empty state through as an empty object. The allowance matches on the error code as well as the status, so a 404 arriving for any other reason still fails.
jamf-cli security dns-custom-hostname-mappings get
# get returns the collection envelope; update wants the bare array
jamf-cli security dns-custom-hostname-mappings get | jq '.results' > mappings.json
# ... edit mappings.json ...
jamf-cli security dns-custom-hostname-mappings update --from-file mappings.json
jamf-cli security dns-custom-hostname-mappings delete --yesThe body is a top-level JSON array that replaces the entire mapping set:
[
{
"aRecords": ["203.0.113.10"],
"aaaaRecords": ["2001:db8::10"],
"hostname": "app.example.com",
"secureDns": false,
"ztna": false
}
]--set builds an object and cannot express this body, so --from-file is the only route. update overwrites the previous mappings (at most 500 entries) and there is no per-entry edit. get returns the standard collection envelope {results, totalCount}, so unwrap .results when round-tripping.
jamf-cli security content-categories listNot paginated. These are the categoryName values that ztna-apps create requires.
jamf-cli security ztna-apps list
jamf-cli security ztna-apps get <appId>
jamf-cli security ztna-apps create --from-file app.json
jamf-cli security ztna-apps patch <appId> --set security.riskControls.levelThreshold=HIGH
jamf-cli security ztna-apps delete <appId> --yes
# The catalog of predefined SaaS app definitions: pass an id as predefinedAppId
jamf-cli security ztna-predefined-apps listEnum values in --help: routing.type (CUSTOM, DIRECT), routing.dnsIpResolutionType (IPv4, IPv6), security.riskControls.levelThreshold (LOW, MEDIUM, HIGH), and the same pair again under groupOverrides.routingOverrides[].routing.*.
Server behaviour the scaffold does not show:
-
categoryNameis required and the scaffold renders it empty, and""is the one value the server rejects (400 [INVALID_FIELD] categoryName: must not be blank).Uncategorizedis a value tenants' own apps carry;content-categories listhas the rest. A category that does not exist is409 MISSING_CATEGORY_NAME. -
Setting
predefinedAppIdinheritsnameandhostnamesfrom the template, and the server drops anameyou send: the create answers201and the app reads backname: null. Those apps cannot be reached with--name, and the failure readsno item with name "…", which also describes a typo. Address them by ID. Reusing a template another app already holds is409 CONFLICT. -
The server accepts an unknown key under
securityand drops it. The schema declaresadditionalProperties: falseand the server does not enforce it, so--set security.riskControls.enabld=trueanswers 201 and does nothing. -
createreturns{id, href}. Follow with agetfor the app's fields.
jamf-cli security ztna-gateways list
jamf-cli security ztna-gateways get <gatewayId>
jamf-cli security ztna-gateways create --from-file gw.json
# Partial IPSec edit, deep-merged server-side, so send only what changes
jamf-cli security ztna-gateways patch <gatewayId> --set ipsec.esp.lifetimeInSec=14400
jamf-cli security ztna-gateways delete <gatewayId> --yes
# Jamf-managed shared gateways available to the tenant (id and name only)
jamf-cli security ztna-shared-gateways list--help lists the full cipher-suite enums (ipsec.{esp,ike}.{dhGroups,encryption,integrity}[]), ipsec.keyExchange (ikev1, ikev2), the 13 datacenter values, and the 11 ipsec.right.vendor values.
-
ipsec.right.vendoris case-sensitive."cisco"for"Cisco"earns a400 INVALID_FIELDcarrying nofieldand the generic "Request body is missing or malformed." The right casing gets per-field errors with attribution. -
ipsec.left.subnetsis capped at one element;right.subnetsis not. Two left subnets earn400 [INVALID_FIELD] field=ipsec "IPSec configuration is not valid.", naming neither the field nor the cardinality. Left subnets must be private (the DNS nameserver rule runs the other way).right.subnetsround-trips more than one, and the server returns them in its own order, so an export/re-apply is not byte-stable. -
contactis required: omitting it is400 [INVALID_FIELD] contact: must not be null. -
patchprints nothing on success. It sendsapplication/merge-patch+jsonand expects204, so there is no body to render. The server-side deep merge is real: patchingipsec.esp.lifetimeInSecalone leavesike,left.subnetsandright.subnetsuntouched. createreturns{id, href}.-
dedicatedIps.ipsis server-allocated and read-only, so the scaffold omits it. - The per-datacenter availability-zone source IPs a peer firewall must allow live in the spec's
availabilityZonesproperty description (specs/platform/securitycloud_ztna_api.json);--helprenders enums and not property descriptions.
jamf-cli security ztna-grouped-gateways list
jamf-cli security ztna-grouped-gateways get <groupedGatewayId>
jamf-cli security ztna-grouped-gateways create --from-file group.json
jamf-cli security ztna-grouped-gateways patch <groupedGatewayId> --set routingStrategy=NEAREST
jamf-cli security ztna-grouped-gateways delete <groupedGatewayId> --yesScaffold:
{
"gatewayIds": ["a1b2", "c3d4"],
"name": "EU Grouped Gateway",
"recoveryDelayInSec": 3600,
"routingStrategy": "",
"tenantIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
}-
recoveryDelayInSecis a required enum of five integers:300,1800,3600,10800,28800. The server rejects0, the value you get by forgetting the field, under every routing strategy including the two whose own prose says the field is ignored. Omitting it or sendingnullis400 … must not be null. -
routingStrategyisACTIVE_STANDBY,RANDOMorNEAREST, and the scaffold renders it empty. - Grouping a gateway that belongs to a shared gateway is
422 SHARED_GATEWAY_MEMBER. createreturns{id, href}.
jamf-cli security device-groups list
jamf-cli security device-groups get <groupId>
jamf-cli security device-groups get --name "Engineering Team"
jamf-cli security device-groups create --set name="Engineering Team"
jamf-cli security device-groups update <groupId> --set name="Engineering"
jamf-cli security device-groups apply --set name="Engineering Team" --yes
jamf-cli security device-groups delete <groupId> --yesA group carries a name and nothing else, so update changes the name and prints nothing on success: it sends PUT /securitycloud/v2/groups/{groupId}, which answers 204 with no body. Read the change back with list. The default group cannot be deleted.
The verbs mix API versions, which shows up in a -vv log or an audit event: list and update are v2, create, get and delete are v1. apply resolves the name against the v2 list, creates on the v1 collection and updates on the v2 item. Verified end to end on an EU environment credential 2026-09-11: create, update --set name=… (silent, exit 0), list showing the new name, delete.
The implicit "Default Group" comes back without an ID.
listreturns it as{"name": "Default Group"}with noid, since it is not a stored group. A group you create carries itsid, so--name,get,updateanddeleteall work on it. On that one name the CLI names the problem:$ jamf-cli security device-groups apply --set "name=Default Group" --yes found 1 item(s) named "Default Group" in /securitycloud/v2/groups, but the list returns no ID for them; identify the item by ID insteadObserved 2026-09-11 on a tenant holding the Default Group plus one created group.
URI_NOT_TEMPLATEDhere is the scope level. Under tenant scope,device-groups listanswers[URI_NOT_TEMPLATED] Failed to (fully) template URI to the downstream service. See logs from the request templater.on
GET /securitycloud/v2/groups, exit code 1, re-confirmed 2026-09-11 on a tenant-scoped credential for the same tenant whose environment-scoped credential returns the group list from that same v2 path. The v2 groups path templates underX-Environment-Idand fails to template underX-Tenant-Id. Use an environment-scoped credential; this is an upstream gateway fault.
Connect a UEM platform to Jamf Security Cloud and manage its device sync.
jamf-cli security uem-connectors list
jamf-cli security uem-connectors get <configId> # no name to resolve; take the id off the list
jamf-cli security uem-connectors create --from-file connector.json
jamf-cli security uem-connectors delete <configId> --yes
jamf-cli security uem-connector-enablement enable <configId> --set enabled=true
jamf-cli security uem-connector-enablement disable <configId>
jamf-cli security uem-sync trigger <configId>
jamf-cli security uem-sync list <configId>
jamf-cli security uem-sync cancel <configId>
jamf-cli security uem-sync-settings get <configId>
jamf-cli security uem-sync-settings update <configId> --from-file settings.json
jamf-cli security uem-activation-profiles deploy-to-uem <code> --from-file deploy.jsonjamf-cli security uem-connectors create --scaffold > connector.json
# ... edit, then ...
jamf-cli security uem-connectors create --from-file connector.json{
"authStrategy": "JAMF_PRO_OAUTH",
"deviceSyncAuth": {
"clientId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"clientSecret": "abcXYZ123secret",
"password": "s3cr3t",
"username": "svc-jsc"
},
"isoCountry": "US",
"tenantId": "acme-tenant",
"url": "https://yourcompany.jamfcloud.com",
"vendor": "JAMF_PRO"
}-
A tenant holds at most one connector, whatever its vendor. A second create answers
409 CONNECTOR_CONFIG_ALREADY_EXISTS, and that check runs after auth-config validation and before the credentials are checked against the UEM instance. -
The request body is a discriminated union over ten vendor shapes, keyed on
vendor.--helpnames all ten and--scaffoldrenders the first,JamfProConnectorCreateRequest, the only wire-verified branch. TheAllowed values:line forauthStrategyspans every variant (JAMF_PRO_OAUTH,BASIC,M2M,USERNAME_PASSWORD,CITRIX_CLOUD_ADMIN,CITRIX_CLOUD_ADMIN_OAUTH) and is wider than the shape the scaffold shows: the last three are Citrix/XenMobile strategies. -
deviceSyncAuthin the scaffold is the union of the OAuth and BASIC credential pairs:clientId/clientSecretandusername/passwordtogether. Delete the half yourauthStrategydoes not use. -
authStrategy: M2Mtakes atenantIdand no credentials: Security Cloud self-provisions what it needs on the named Jamf Pro, andurlis then ignored. The Jamf Pro API role and integration it provisions survive the connector'sDELETE, so repeated create/delete cycles leak an enabled integration per create onto the target instance, with nothing in the connector response pointing at one.jamf-cli pro api-integrations listagainst the instance, on an instance profile (a gateway profile refuses that command), is the only way to see them. -
Put the secret in
--from-file, never in--set.--set deviceSyncAuth.clientSecret=…puts it in shell history andpsoutput. - Error ladder, in order: no
authStrategy→500 INTERNAL_ERROR;authStrategywith nodeviceSyncAuth→422 VALIDATION_FAILED ": invalid auth configuration for Jamf PRO"; a complete body on a tenant that already has a connector →409 CONNECTOR_CONFIG_ALREADY_EXISTS. - A read returns
clientIdandusernameand no secret, so aJAMF_PRO_OAUTHconnector cannot be exported and re-created. AnM2Mone can, sincetenantIdis on both the request and the response.
# Read the current values...
jamf-cli security uem-sync-settings get <configId>
# ...but build the body from the scaffold: get and update do NOT share a shape
jamf-cli security uem-sync-settings update --scaffold > settings.json
# ... edit settings.json ...
jamf-cli security uem-sync-settings update <configId> --from-file settings.json- It replaces the whole settings object, so a field you omit resets to its default. Start from the current values.
-
getandupdatedo not share a shape.getreturns the whole connector config (id,customerId,connected,latestSync,url,deviceSyncAuthand the rest), withautoDeviceDeletionanddisableSyncOnAuthErrornested undersyncConfig.updateaccepts ten fields, with those two flat. Readgetoutput and transcribe into the scaffold; piping one into the other fails. -
refreshRateMinutesenforces its enum:60,120,240,480,720,1440. Anything else (0,1and360included) is422 VALIDATION_FAILED, and the message leaks the accepted set while leavingfieldnull. -
The server discards
deviceUnmanagedThresholdforJAMF_PROconnectors. It answers204and reads back0whatever you sent, so a grace period set on a Jamf Pro connector does not exist, while--helplists the enum (0, 1, 3, 5, 7, 14) as if it applied. On a vendor that honours it, the value is days since last check-in and0means the platform default of 3 days. -
The body's
vendormust match the connector's stored vendor: a mismatch is422 VENDOR_MISMATCH. The field selects which vendor-specific settings apply; the<configId>path selects the connector, and a connector's vendor cannot be changed here. The spec's operation description carries this and--helprenders only its first paragraph. -
concurrentSyncEnabledis writable and part of the replacement, so omitting it resets it to its default.
-
uem-connector-enablement enable <configId>takes a body ({"enabled": true});disabletakes none. Both are idempotent. -
uem-sync triggerreturns202 Acceptedand the sync runs in the background. Polluem-sync list <configId>for progress (newest first).uem-sync cancelis best-effort; the run may finish before it takes effect. -
uem-activation-profiles deploy-to-uem <code>is the last step of automated UEM Connect onboarding: it pushes the Jamf Security configuration profiles to the UEM platform for one activation-profile code. Enums:platform(SUPERVISED_MAC,SUPERVISED_IOS,UNSUPERVISED_IOS,BYOD_IOS),uem(JAMF). - Only
uem-sync listdeclares pagination parameters upstream; most Security Cloud lists ignorepage/page-sizeand return everything.
The object uem-activation-profiles deploy-to-uem deploys. This resource mints and manages the code; UEM Connect holds the deploy action.
# --origin is required, and PUBLIC_API is its only legal value
jamf-cli security eap list --origin PUBLIC_API
jamf-cli security eap create --scaffold > profile.json
# ... edit profile.json ...
jamf-cli security eap create --from-file profile.json
jamf-cli security eap get <activationProfileId>
jamf-cli security eap pause <activationProfileId>
jamf-cli security eap resume <activationProfileId>
jamf-cli security eap delete-multiple --set codes='["<code>"]' --yesThe create scaffold:
{
"capabilities": {
"dataPolicy": false,
"networkSecurity": false,
"note": "Enabled for compliance requirements",
"vulnerabilityManagement": false
},
"groupId": "group-123",
"name": "My Activation profile",
"origin": "PUBLIC_API",
"platforms": ["iOS"]
}Server behaviour, none of it visible from the spec:
-
The read model is a
codeand nothing else.getreturns{"code": "…"}and omits the name, platforms, capabilities and state the create sent.listunwraps to a bare array of the same one-field objects. A create's input cannot be read back. -
createanswers{"code": "…"}, despite declaring an{id, href}body with aLocationheader. Take the code; there is noidto read. -
pauseandresumehave no observable effect on any GET, and print nothing on success (both are204-no-body and idempotent). -
delete-multipleis a soft delete the read surface does not reflect. After it answers204,get <code>still answers200andliststill returns the code. A write reveals it:pauseon a deleted code answers409 STATE_CONFLICT("… is already deleted."), in a framework error envelope carrying notraceId. You can neither confirm a delete nor filter deleted profiles out of a list, and each profile created is a permanent row. -
capabilities.networkSecurityandcapabilities.vulnerabilityManagementmust be both on or both off:400 [INVALID_FIELD] capabilities: networkSecurity and vulnerabilityManagement must both be enabled or both disabled. -
platformsacceptsiOSandMAConly, and the server applies the 1–2 size limit after de-duplication, so["iOS","MAC","iOS"]is accepted. -
groupIdis not validated. A mistyped group answers201with the profile scoped to nothing.capabilities.note's length limit is unenforced too. -
listrequires--origin, and cobra refuses the call client-side without it. The two halves report an out-of-enum origin differently:400 … Unknown origin value.onlist,400 … Origin not provided.oncreate, which misreports the cause.
| Command | Action | API | Destructive |
|---|---|---|---|
security setup |
Configure Radar credentials | n/a | |
security risk list |
List device risk status | Radar | |
security risk override |
Override calculated device risk | Radar | |
security device-lifecycle purge |
Purge devices | Radar | ✓ |
security stream get |
Get stream configuration | Radar | |
security stream update |
Create/replace stream configuration | Radar | |
security stream delete |
Delete stream configuration | Radar | ✓ |
security status get |
Get stream status | Radar | |
security status update |
Update stream status | Radar | |
security verification trigger |
Trigger stream verification | Radar | |
security well-known get |
Get transmitter discovery document | Radar | |
security jwks get |
Get transmitter JSON Web Key Set | Radar | |
security dns-zones list / get / create / apply / patch / delete
|
DNS zone CRUD | Gateway | delete |
security dns-search-domains get / update / delete
|
Tenant search domain (singleton) | Gateway | delete |
security dns-custom-hostname-mappings get / update / delete
|
Custom hostname mappings (whole-list) | Gateway | delete |
security content-categories list |
List content categories | Gateway | |
security ztna-apps list / get / create / apply / patch / delete
|
ZTNA access policies | Gateway | delete |
security ztna-predefined-apps list |
Predefined SaaS app catalog | Gateway | |
security ztna-gateways list / get / create / apply / patch / delete
|
Dedicated ZTNA gateways | Gateway | delete |
security ztna-shared-gateways list |
Jamf-managed shared gateways | Gateway | |
security ztna-grouped-gateways list / get / create / apply / patch / delete
|
Grouped ZTNA gateways | Gateway | delete |
security device-groups list / get / create / apply / update / delete
|
Security Cloud device groups | Gateway | delete |
security uem-connectors list / get / create / delete
|
UEM connector | Gateway | delete |
security uem-connector-enablement enable / disable
|
Pause/resume connector sync | Gateway | |
security uem-sync trigger / list / cancel
|
Sync runs | Gateway | |
security uem-sync-settings get / update
|
Connector sync settings | Gateway | |
security uem-activation-profiles deploy-to-uem |
Push JSC profiles to the UEM | Gateway | |
security enrollment-activation-profiles list / get / create / pause / resume / delete-multiple
|
Enrollment activation profiles | Gateway | delete-multiple |
-
The Radar half has no name-to-ID resolution. Supply every identifier (
guid,externalId,deviceIds) directly. The gateway half has--nameon mostget/patch/update/deletecommands, resolved by listing the collection and matching across every page. Four exceptions: a predefined-derived ZTNA app (whose name isnullon the wire), the UEM connectors (whose read model carries anidand no name), the enrollment activation profiles (where the read model is acode, so the flag is not emitted), and the implicit device-group "Default Group", which the list returns with noidand the CLI reports as such. -
Body writes take
--from-file(JSON or YAML) and/or--set key=value, with--scaffoldto print the template.--setoverlays onto a--from-filebody. Leave--from-fileoff and the body is read from stdin, so--scaffold | edit | applypipes end to end. See CLI Patterns. -
Privileges. The gateway half carries per-operation permissions the catalog reports:
ztna:*(also used by DNS zones),content-categories:read,device-groups:*,search-domains:*,custom-hostname-mappings:*,uem-connect:*,activation-profiles:*. List them withjamf-cli commands -o json | jq -r '.[] | select(.product=="security") | .command + " " + ((.privileges // []) | join(","))'. -
A 403 names the permission in Jamf Account's own words and exits 5:
{ "error": "permission_denied", "exitCode": 5, "exitCodeName": "permission_denied", "hint": "grant the Jamf Platform API integration these permissions in Jamf Account: Secure enterprise access > Content categories: Read (content-categories:read). Names are as the permission picker shows them: https://developer.jamf.com/platform-api/reference/jamf-pro-permissions-map", "message": "list: API request failed with status 403, traceId 213a…e4f7 (method=GET, url=https://eu.api.jamfcloud.com/securitycloud/v1/categories): [BAD_PERMISSIONS] The given token was not authorized to access the requested resource." }You search the picker by name, which is why the hint prints the name alongside the slug.
security dns-zones listgives the same shape with Secure enterprise access > Zero-Trust Network Access (ZTNA): Read (ztna:read): DNS zones need the ztna grant, which the command name does not suggest. -
A
403 BAD_PERMISSIONSon the gateway half is an answer about grants. It says the integration in hand lacks the permission, and says nothing about the endpoint or the scope level.jamf-cli platform setupdoes not answer it either: it validates the credentials and the scope ID, reports which resource groups the credential's level declares, and points at the 403, which names the permission. -
Output defaults to JSON. Add
-o table/-o yaml/-o csvas needed. See Output Formats. -
Exit codes, including 8 (refused by policy) and 5 (a platform 403), are in Error Handling & Exit Codes.
For copy-paste recipes, see Security Cloud Workflows. For the gateway itself (regions, scope levels, the GA base URL) see Platform API GA Migration and Jamf Platform Commands.
Repository · Issues · Releases
jamf-cli Wiki
- Home
- Community
- Getting Started
- CLI Reference
- Product Commands
- Workflows
- Configuration
- Reference
Products
- Jamf Pro:
jamf-cli pro - Jamf Platform API:
jamf-cli pro(blueprints, benchmarks, DDM reports) - Jamf Platform:
jamf-cli platform(AI Governance, Jamf Account, audit) - Jamf Protect:
jamf-cli protect - Jamf School:
jamf-cli school - Jamf Security Cloud:
jamf-cli security