CXone Admin API bulk update for agent skill proficiencies failing with 400

Trying to script a bulk update for agent skill proficiencies via the CXone Admin API. We’ve got a new routing model rolling out and need to adjust the proficiency levels for about 500 agents based on a CSV export from our WFM team. The individual PUT calls work fine when I test them manually in Postman, but I need to automate this.

I’m hitting the /api/v2/users/{userId}/skills endpoint. The docs show a request body like this:

[
 {
 "skillId": "12345",
 "proficiency": 50
 },
 {
 "skillId": "67890",
 "proficiency": 30
 }
]

I’m looping through the users in Python and sending the payload. The issue is that when I hit the endpoint, I get a 400 Bad Request with a vague message: Invalid request body. Please check the documentation.

Here’s the snippet of what I’m sending:

import requests

headers = {
 "Authorization": f"Bearer {token}",
 "Content-Type": "application/json"
}

payload = [
 {"skillId": "11111", "proficiency": 100},
 {"skillId": "22222", "proficiency": 50}
]

response = requests.put(
 f"https://api.nicecxone.com/api/v2/users/{user_id}/skills",
 headers=headers,
 json=payload
)

print(response.status_code)
print(response.text)

The skillId values are definitely valid. I’ve double-checked them against the skills endpoint. I also tried adding skillName to the object, but that didn’t change anything. The error persists.

Is there a specific format requirement for the bulk update that’s not obvious from the docs? Or am I hitting a rate limit that’s masking the real error? The response body doesn’t give much to go on besides the generic 400 message.

Any ideas on what I’m missing here? I’ve been staring at this for two hours.

You’re hitting the 400 because you’re likely sending a full list of skills instead of the delta or missing the specific proficiency structure. The /api/v2/users/{userId}/skills endpoint is finicky. It doesn’t accept a bulk array of updates in a single call for multiple agents. You have to iterate.

Also, check your JSON structure. If you’re passing proficiencyLevel as a string, it fails. It needs to be an integer. And you can’t just update one field; you usually need to pass the entire skill object back with the updated value, or use the add/remove actions if you’re just toggling.

Here’s how I handle it in Studio using a REST Proxy action. It’s safer than trying to loop in Python and hitting rate limits.

{
 "method": "PUT",
 "uri": "/api/v2/users/{{userId}}/skills",
 "body": {
 "skills": [
 {
 "id": "{{skillId}}",
 "proficiencyLevel": 85,
 "proficiencyType": "percentage"
 }
 ]
 }
}

Make sure proficiencyType matches what’s defined on the skill itself. If it’s numeric, send the number. If it’s percentage, send 0-100. Mixing those up causes silent failures or 400s.

Don’t forget to throttle your requests. 500 agents is a lot. If you fire these off in a loop without delay, CXone will throttle you. Add a small delay between iterations in your script or use a queue.

One more thing: verify the OAuth token has user:update scope. If you’re using an admin token, it should have it, but if you’re using a delegated token, check the scopes.

The CSV from WFM might have skill names, not IDs. You’ll need to resolve those names to IDs first. Query /api/v2/users/skills to get the mapping. Then join that data to your agent list.

It’s tedious, but it works. I’ve done this for 2k agents before. Just be patient with the API.