Bulk-updating agent skill proficiencies via CXone Admin API: 400 Bad Request on nested skill array

Hey everyone,

I’m trying to bulk-update skill proficiencies for a batch of agents using the CXone Admin API. My goal is to inject trace context into these calls for our OTel pipeline, but the payload structure is throwing me off.

I’m hitting PUT /api/v2/admin/users/{userId}/skills with a JSON body like this:

{
 "skills": [
 {
 "skillId": "abc-123",
 "proficiency": "expert"
 },
 {
 "skillId": "def-456",
 "proficiency": "intermediate"
 }
 ]
}

I keep getting a 400 Bad Request with the message: Invalid skill proficiency value. The docs say proficiency should be a string, but I’ve tried enums, integers, and even the full object shape.

Is there a specific format for the proficiency field? Or am I missing a required header for bulk operations? I’ve verified the skillIds are valid and the user has permission.

Here’s the curl snippet I’m using for testing:

curl -X PUT "https://{{env}}.mypurecloud.com/api/v2/admin/users/{{userId}}/skills" \
 -H "Authorization: Bearer {{token}}" \
 -H "Content-Type: application/json" \
 -d '{"skills":[{"skillId":"abc-123","proficiency":"expert"}]}'

Any insights?

You’re hitting a 400 because the proficiency field expects a number, not a string. The API docs are a bit light on the type enforcement, but the schema is strict. You’ll also need to make sure the skillId matches the actual NICE CXone skill ID, not a custom attribute or CRM ID.

Here’s the correct payload structure. Note the proficiency is an integer (0-100).

{
 "skills": [
 {
 "skillId": "abc-123",
 "proficiency": 90
 },
 {
 "skillId": "def-456",
 "proficiency": 50
 }
 ]
}

If you’re doing this in bulk via a script, watch out for rate limits on the /admin/users endpoint. It’s not as forgiving as the contact center APIs. Also, double-check your trace context headers. If you’re injecting them into the HTTP request, ensure they don’t interfere with the Authorization header or the Content-Type: application/json. I’ve seen custom headers cause silent failures on some proxy configurations.

One more thing. If you’re using the NICE CXone SDK, check if there’s a wrapper method for this. Sometimes the raw REST call misses some implicit validations that the SDK handles. But honestly, the raw PUT is usually faster for bulk ops if you batch correctly. Just don’t fire off 100 requests in parallel. The API will throttle you.

nailed the proficiency type issue, but you’ll likely hit another wall with that endpoint if you’re trying to do a true bulk update. The PUT /api/v2/admin/users/{userId}/skills path only handles one agent at a time. Sending a list of skills for a single user works, but you can’t pass an array of user IDs in that call.

If you’re looping through 500 agents, you’re going to get rate-limited hard. The CXone API caps admin writes pretty aggressively. You’ll want to switch to the async bulk update endpoint instead. It accepts a CSV file or a JSON list of updates and processes them in the background.

Here’s how you structure the request for the bulk operation. You post to /api/v2/admin/users/bulkupdate and set the operation to update. The payload expects an array of objects where each object contains the id of the user and the skills array we discussed.

{
 "operation": "update",
 "users": [
 {
 "id": "user-id-1",
 "skills": [
 {
 "skillId": "abc-123",
 "proficiency": 90
 },
 {
 "skillId": "def-456",
 "proficiency": 75
 }
 ]
 },
 {
 "id": "user-id-2",
 "skills": [
 {
 "skillId": "abc-123",
 "proficiency": 80
 }
 ]
 }
 ]
}

Make sure you include the X-Request-ID header if you’re tracing this through OTel. The async job returns a job ID immediately. You can poll /api/v2/admin/users/bulkupdate/{jobId} to check status. It usually takes a few minutes for large batches.

One thing to watch out for is that this replaces the entire skill set for the user in that call. If you only want to update one skill, you still need to include the others you want to keep, otherwise they’ll get wiped. The API doesn’t merge partial skill lists on this endpoint. You might want to fetch the current skills first if you’re doing incremental updates.

Also, check your OAuth scope. You need admin:users:write and admin:users:bulkupdate. Missing the bulk scope will give you a 403 instead of a 400, which is confusing when debugging.