CXone Admin API: Bulk skill proficiency update returns 409 Conflict for existing users

Something is weird with the behavior of the PATCH /api/v2/users/{userId}/skills endpoint. I’m trying to automate the bulk update of agent skill proficiencies based on a nightly CSV feed from our WFM system. The goal is to set the proficiency level for specific skills without overwriting other existing skills.

Here is the setup:

  • Endpoint: PATCH /api/v2/users/{userId}/skills
  • Auth: OAuth2 Client Credentials with admin:skill:write scope.
  • Payload: A list of skill objects with id and proficiency.

The first run of the script works fine. It updates the proficiencies for 50 agents. I run it again an hour later with the same data (no changes in the CSV), and suddenly I get a 409 Conflict for about 10% of the users.

The error payload looks like this:

{
 "code": "duplicate",
 "message": "Skill assignment already exists for user."
}

I checked the documentation. It says PATCH should be idempotent or at least handle existing assignments gracefully by updating the value. It shouldn’t throw a conflict if the skill is already assigned, especially if I’m just updating the proficiency level.

I’ve tried the following:

  • Verifying the proficiency value is an integer (1-5) as expected.
  • Checking if the skill id is correct. It matches the skill ID returned by the search API.
  • Adding a delay between requests to avoid rate limiting, though the error is 409, not 429.
  • Using PUT instead of PATCH. This fails with a 400 Bad Request saying the request body is invalid, which makes sense because PUT usually expects the full list of skills, and I only want to update specific ones.

Is this a known bug with the Admin API? Or am I missing a header or query parameter that tells the API to “update if exists”? The script is written in Python using the requests library. The code is standard:

headers = {
 'Authorization': f'Bearer {token}',
 'Content-Type': 'application/json'
}
data = [{"id": skill_id, "proficiency": level}]
response = requests.patch(f'https://{org_id}.mypurecloud.com/api/v2/users/{user_id}/skills', headers=headers, json=data)

The weird part is that it works for some users and fails for others in the same batch. The users who fail are not special in any way. They are all active agents with the same role.