Policy changes
Change the settings of an Android policy, create a policy as a copy of another, or roll a policy back to an earlier revision. Every change is reviewed before it is applied.
How it works
Every change takes two steps. First you propose it: the API records a proposal and returns its change list, with each setting's current and new value, the impact and when the proposal expires. Nothing changes on the policy yet. Then you apply the proposal by its proposalId, after the person responsible has reviewed the change list.
Only the API key that made a proposal can read or apply it. For an OAuth connection, only the same connection and user can. A proposal expires 10 minutes after it is made; the expiresAt field gives the exact time.
Only Android policies can be changed this way. Policies managed by Nomid cannot. Creating a policy also requires the company to be connected to Android Enterprise.
Requirements
- An API key with the POLICIES_WRITE permission (Policy changes), combined with POLICIES_READ. Only a portal user with permission to edit policies can create a key with POLICIES_WRITE. Required by the four POST endpoints and the MCP policy change tools.
- The company setting Policy changes, under AI agent access in the portal, must be turned on. It is off by default. While it is off, every proposal and apply call returns 403. Reading settings, revisions and proposals needs POLICIES_READ only.
- MCP clients that connect with OAuth instead of an API key need the mcp:policies:write scope.
Endpoints
All paths are relative to the API base URL and need the X-API-Key header. Request bodies are JSON.
| Method | Endpoint | Description |
|---|---|---|
| GET | /policies/{policyId}/settings | The policy's editable settings, each with its current value, allowed values, group and whether changing it is high impact. Also returns the policy version, deviceCount and whether the policy is editable. |
| GET | /policies/{policyId}/revisions | The policy's saved revisions, newest first, with when and by whom each was saved. limit is optional, from 1 to 100, default 20. |
| GET | /policy-proposals/{proposalId} | A proposal made by this key, with its change list and status: PENDING, EXECUTING, SUCCEEDED, FAILED, UNCONFIRMED or EXPIRED. |
| POST | /policies/{policyId}/changes | Propose setting changes to an existing policy. Returns 201 with the proposal. |
| POST | /policies | Propose a new policy as a copy of an existing one, with optional setting changes. Without changes it duplicates the policy. Returns 201 with the proposal. |
| POST | /policies/{policyId}/rollbacks | Propose restoring the policy to an earlier revision. Returns 201 with the proposal. |
| POST | /policy-proposals/{proposalId}/apply | Apply a pending proposal. Returns 200 with the proposal and its result: policyId, displayName, version and a portal link. |
Request bodies
Change settings
| Field | Required | Description |
|---|---|---|
| changes | Yes | Array of objects with setting (a setting ID) and value (one of its allowed values). Each setting at most once. Values are not case-sensitive. |
Create a policy
| Field | Required | Description |
|---|---|---|
| fromPolicyId | Yes | The policy to copy, by its policy ID (pathName). The copy takes its apps, settings and restrictions. |
| displayName | Yes | Name of the new policy as shown in the portal, at most 50 characters. |
| changes | No | Setting changes to apply to the copy, in the same format as above. Leave it out to duplicate the policy. No device uses the new policy until you move devices to it. |
Roll back a policy
| Field | Required | Description |
|---|---|---|
| revision | Yes | The revision number to restore, from the revisions endpoint. The policy's description, tags and group stay as they are. Its name goes back to the revision's name when they differ, and the change list shows it. |
Apply a proposal
| Field | Required | Description |
|---|---|---|
| confirmation | For HIGH impact | Required when the proposal has requiresConfirmation set to true: the policy's name, typed by the person approving the change. Matching ignores letter case and surrounding spaces. |
The proposal
Every propose endpoint and the get and apply endpoints return the proposal. The most useful fields:
| Field | Description |
|---|---|
| proposalId | The ID to read or apply the proposal with. |
| status | PENDING until applied, and EXECUTING while an apply is running: neither is final. Then SUCCEEDED or FAILED, UNCONFIRMED when an unexpected error left the outcome unknown, or EXPIRED once expiresAt has passed. |
| impact | NORMAL or HIGH. See Impact and confirmation. |
| requiresConfirmation | True for a HIGH impact proposal: apply needs the confirmation field. |
| fields | The change list. Each entry has label (the setting's name), value (the new value) and previousValue (the current value). |
| summary | One sentence describing the change, including how many devices use the policy. |
| target | The policy the proposal changes: kind, id and name. |
| expiresAt | When the proposal expires. After that it can no longer be applied. |
| result | Apply responses only: the policy as it is now, with policyId, displayName, version, a message and portalUrl. |
Impact and confirmation
A proposal is HIGH impact when the policy has at least one device and any of these is true:
- It is a rollback.
- It changes a high-impact setting: apps from unknown sources, Google Play Protect, factory reset, developer options, USB data transfer or storage encryption.
- The policy has 50 or more devices, whatever the change.
Everything else is NORMAL, including every new policy, since no device uses it yet.
Proposals are pinned to a policy version
A proposal records the policy version its change list was built on. If the policy is saved again before you apply the proposal, by anyone and from anywhere, apply fails with 409 and code stale_proposal, and nothing changes. Read the policy again, propose again and review the new change list.
Example: propose and apply
Read the policy's current settings first, to get the setting IDs and allowed values:
curl "https://api.nomid.tech/emm/api/v1/policies/p7k2m9qa4xz/settings" \
-H "X-API-Key: $NOMID_API_KEY" {
"policyId": "p7k2m9qa4xz",
"displayName": "Warehouse scanners",
"version": 12,
"deviceCount": 18,
"editable": true,
"settings": [
{
"id": "CAMERA",
"label": "Camera",
"group": "RESTRICTIONS",
"value": "ALLOW",
"allowedValues": ["ALLOW", "BLOCK"],
"highImpact": false
}
]
} Propose blocking the camera and USB data transfer:
curl -X POST "https://api.nomid.tech/emm/api/v1/policies/p7k2m9qa4xz/changes" \
-H "X-API-Key: $NOMID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"changes": [{"setting": "CAMERA", "value": "BLOCK"}, {"setting": "USB_DATA_ACCESS", "value": "BLOCK"}]}' HTTP/1.1 201 Created
{
"capability": "update_policy_settings",
"status": "PENDING",
"title": "Edit policy settings",
"summary": "Change 2 settings of policy \"Warehouse scanners\". 18 devices apply it on their next sync. High impact: type the policy's name to confirm.",
"target": { "kind": "policy", "id": "p7k2m9qa4xz", "name": "Warehouse scanners" },
"fields": [
{ "label": "Camera", "value": "BLOCK", "previousValue": "ALLOW" },
{ "label": "USB data transfer", "value": "BLOCK", "previousValue": "ALLOW" }
],
"expiresAt": "2026-10-09T14:40:00Z",
"impact": "HIGH",
"proposalId": "prp_7GQv2LkR9sTn4WxY8bZcA1dE",
"requiresConfirmation": true
} Abridged response. USB data transfer is a high-impact setting and the policy has devices, so the proposal is HIGH impact and requires confirmation. Show the change list to the person responsible before applying it.
Once they approve and type the policy's name, apply the proposal with what they typed:
curl -X POST "https://api.nomid.tech/emm/api/v1/policy-proposals/prp_7GQv2LkR9sTn4WxY8bZcA1dE/apply" \
-H "X-API-Key: $NOMID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"confirmation": "Warehouse scanners"}' HTTP/1.1 200 OK
{
"capability": "update_policy_settings",
"status": "SUCCEEDED",
"impact": "HIGH",
"proposalId": "prp_7GQv2LkR9sTn4WxY8bZcA1dE",
"requiresConfirmation": true,
"result": {
"policyId": "p7k2m9qa4xz",
"displayName": "Warehouse scanners",
"version": 13,
"message": "Policy settings changed. Devices on the policy apply them on their next sync."
}
} Abridged response. Devices on the policy apply the new settings on their next sync.
When the policy changed in the meantime
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"detail": "Policy \"Warehouse scanners\" was changed after this proposal was made. Nothing was applied: read it again and propose again.",
"instance": "/emm/api/v1/policy-proposals/prp_7GQv2LkR9sTn4WxY8bZcA1dE/apply",
"code": "stale_proposal",
"retryable": false
} Editable settings
These are the setting IDs accepted in changes, with their allowed values. The label is the name the API returns in the change list.
When reading settings, a value can also be NOT_SET (left to the device or Google's default) or CUSTOM (a value set in the portal that these values cannot express). Neither can be sent, but both can be replaced with an allowed value.
| Setting | Label | Group | Allowed values | High impact |
|---|---|---|---|---|
| PLAY_STORE_MODE | Play Store mode | APPS | BLACKLIST, WHITELIST | No |
| DEFAULT_PERMISSION_POLICY | Default runtime permission policy | APPS | DENY, PROMPT | No |
| APP_AUTO_UPDATE_POLICY | App auto-update policy | APPS | ALWAYS, CHOICE_TO_THE_USER, NEVER, WIFI | No |
| UNTRUSTED_APPS_POLICY | Apps from unknown sources | SECURITY | ALLOW_INSTALL_IN_PERSONAL_PROFILE_ONLY, DISALLOW_INSTALL | Yes |
| PLAY_PROTECT | Google Play Protect app verification | SECURITY | ENABLED, USER_CHOICE | Yes |
| SCREEN_CAPTURE | Screen capture | RESTRICTIONS | ALLOW, BLOCK | No |
| CAMERA | Camera | RESTRICTIONS | ALLOW, BLOCK | No |
| FACTORY_RESET | Factory reset from Settings | RESTRICTIONS | ALLOW, BLOCK | Yes |
| UNINSTALL_APPS | Uninstalling apps | RESTRICTIONS | ALLOW, BLOCK | No |
| ACCOUNT_MODIFICATION | Adding or removing accounts | RESTRICTIONS | ALLOW, BLOCK | No |
| ADD_USER | Adding users | RESTRICTIONS | ALLOW, BLOCK | No |
| REMOVE_USER | Removing users | RESTRICTIONS | ALLOW, BLOCK | No |
| DEVELOPER_SETTINGS | Developer options and USB debugging | SECURITY | ALLOW, BLOCK | Yes |
| USB_DATA_ACCESS | USB data transfer | CONNECTIVITY | ALLOW, BLOCK | Yes |
| LOCATION_MODE | Location | LOCATION | DISABLED, ENFORCED, USER_CHOICE | No |
| LOCATION_SHARING | Sharing location | LOCATION | ALLOW, BLOCK | No |
| OUTGOING_CALLS | Outgoing calls | RESTRICTIONS | ALLOW, BLOCK | No |
| SMS | SMS | RESTRICTIONS | ALLOW, BLOCK | No |
| BLUETOOTH | Bluetooth | CONNECTIVITY | ALLOW, BLOCK | No |
| DATA_ROAMING | Data roaming | CONNECTIVITY | ALLOW, BLOCK | No |
| NETWORK_RESET | Network settings reset | CONNECTIVITY | ALLOW, BLOCK | No |
| VPN_CONFIGURATION | Configuring VPNs | CONNECTIVITY | ALLOW, BLOCK | No |
| CONFIGURE_WIFI | Configuring Wi-Fi networks | CONNECTIVITY | ALLOW, BLOCK | No |
| WIFI_DIRECT | Wi-Fi Direct | CONNECTIVITY | ALLOW, BLOCK | No |
| TETHERING | Tethering and hotspot | CONNECTIVITY | ALLOW, BLOCK | No |
| WIFI_STATE | Wi-Fi on or off | CONNECTIVITY | DISABLED, ENABLED, USER_CHOICE | No |
| AIRPLANE_MODE | Airplane mode | CONNECTIVITY | DISABLED, USER_CHOICE | No |
| MINIMUM_WIFI_SECURITY | Minimum Wi-Fi security | CONNECTIVITY | OPEN_NETWORK, PERSONAL_NETWORK | No |
| AUTO_DATE_TIME | Automatic date, time and time zone | RESTRICTIONS | ENFORCED, USER_CHOICE | No |
| ENCRYPTION_POLICY | Storage encryption | SECURITY | ENABLED_WITHOUT_PASSWORD, ENABLED_WITH_PASSWORD, UNSPECIFIED | Yes |
| APPLICATION_REPORT_LEVEL | Installed apps reporting | REPORTING | DISABLED, INSTALLED_AND_REMOVED_APPS, INSTALLED_APPS | No |
| REPORT_DEVICE_SETTINGS | Device settings reporting | REPORTING | DISABLED, ENABLED | No |
| REPORT_DISPLAY_INFO | Display information reporting | REPORTING | DISABLED, ENABLED | No |
| REPORT_HARDWARE_STATUS | Hardware status reporting | REPORTING | DISABLED, ENABLED | No |
| REPORT_MEMORY_INFO | Memory information reporting | REPORTING | DISABLED, ENABLED | No |
| REPORT_NETWORK_INFO | Network information reporting | REPORTING | DISABLED, ENABLED | No |
| REPORT_POWER_EVENTS | Power events reporting | REPORTING | DISABLED, ENABLED | No |
| REPORT_SOFTWARE_INFO | Software information reporting | REPORTING | DISABLED, ENABLED | No |
| REPORT_SYSTEM_PROPERTIES | System properties reporting | REPORTING | DISABLED, ENABLED | No |
MCP tools
The MCP server exposes the same operations as tools. They take the same arguments as the REST endpoints, with policyId and proposalId as arguments instead of path segments.
| Tool | Permission | Description |
|---|---|---|
| get_policy_settings | POLICIES_READ | Read a policy's editable settings with their current and allowed values. |
| list_policy_revisions | POLICIES_READ | List a policy's saved revisions, newest first. |
| get_policy_proposal | POLICIES_READ | Read a proposal made by the same key or connection, with its status. |
| update_policy_settings | POLICIES_WRITE + POLICIES_READ | Propose setting changes to an existing policy. |
| create_policy | POLICIES_WRITE + POLICIES_READ | Propose a new policy as a copy of an existing one, with optional setting changes. |
| rollback_policy | POLICIES_WRITE + POLICIES_READ | Propose restoring a policy to an earlier revision. |
| apply_policy_proposal | POLICIES_WRITE + POLICIES_READ | Apply a pending proposal by its proposalId, with confirmation when it is HIGH impact. |
An assistant must show the user the change list and get their explicit approval in the conversation before it calls apply_policy_proposal. For a HIGH impact proposal it must ask the user to type the policy's name and pass exactly what they typed.
Errors
Once the API key is authenticated, errors use application/problem+json with two extra fields: code, a stable identifier to branch on, and retryable, which says whether the same request can be sent again. A missing or invalid key gets the shared 401 response, a small JSON object with error, message and status. MCP tools return the same code and message as a tool error.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_arguments | The body is invalid: an unknown setting, a value that is not allowed, a setting listed twice, or a missing field. The detail says which. |
| 403 | forbidden | The key lacks POLICIES_WRITE or POLICIES_READ, or the company setting Policy changes is off. |
| 404 | not_found | No such policy or revision visible to this key, or no proposal with this ID made by this key. |
| 409 | unsupported | The policy cannot be changed this way: it is not an Android policy, or it is managed by Nomid. |
| 409 | not_pending | The proposal is no longer PENDING. Read it to see its status: if it is SUCCEEDED, the change is made. If it is EXECUTING, read it again until it finishes. If it is UNCONFIRMED, read the policy's settings before proposing again. If it is FAILED or EXPIRED, propose again. |
| 409 | stale_proposal | The policy was saved after the proposal was made. Nothing was applied. Read the policy again and propose again. |
| 422 | refused | The request cannot be carried out: nothing would change, the company is not connected to Android Enterprise, or the new policy name cannot be used. |
| 422 | confirmation_required | The proposal is HIGH impact and confirmation is missing or does not match the policy's name. Nothing was applied, and the proposal stays pending until it expires. |
| 429 | rate_limited | Too many write requests for this key. The Retry-After header and the detail give the seconds to wait. retryable is true. |
| 500 | internal_error | Unexpected error. Read the proposal before retrying: if its status is FAILED, propose again. If it is UNCONFIRMED, read the policy's settings to see whether the change was made before proposing it again. |
Rate limits
Each propose and apply call counts toward the per-key write limit of 10 requests per minute, shared with the device command endpoints and the MCP write tools. Read calls do not count toward it.
Revisions and rollback
Every applied change saves a new revision of the policy. List revisions to see who changed the policy and when, and propose a rollback to undo a change.