PowerShell/API to Bulk Reassign Selected Entitlements to Different Reviewers in Certification Campaign

Hi everyone,
Has anyone worked with a PowerShell script or API-based solution for bulk reassigning selected entitlements/access items within an active ISC certification campaign?
For example, assume a campaign contains 9 entitlements:

  • Entitlements 1, 2, 3 → Reviewer 1

  • Entitlements 4, 5, 6 → Reviewer 2

  • Entitlements 7, 8, 9 → Reviewer 3

    Has anyone implemented this with PowerShell + API, and can you share an example?

Hi @Rosha ,

From my point of view, this is achievable with the Certifications API. Here’s the approach I’d take:

  1. Get the certifications in the campaign with GET /certifications/v1 (filter on the campaign ID). Reassignment happens per certification.
  2. List the items with GET /certifications/v1/{certId}/access-review-items and map your entitlements to the returned item IDs.
  3. Reassign per target reviewer with POST /certifications/v1/{certId}/reassign. A single call handles at most 50 items, so batch larger sets. Please refer below sample PowerShell script
$base  = "https://<tenant>.api.identitynow.com"
$token = "<bearer token>"
$hdr   = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$certId = "<certification id>"

# 1. Get review items
$items = Invoke-RestMethod -Method Get -Headers $hdr `
  -Uri "$base/certifications/v1/$certId/access-review-items?limit=250"

# 2. Entitlement name -> reviewer identity ID
$assignments = @{
  "Entitlement 1" = "<reviewer1Id>"; "Entitlement 2" = "<reviewer1Id>"; "Entitlement 3" = "<reviewer1Id>"
  "Entitlement 4" = "<reviewer2Id>"; "Entitlement 5" = "<reviewer2Id>"; "Entitlement 6" = "<reviewer2Id>"
  "Entitlement 7" = "<reviewer3Id>"; "Entitlement 8" = "<reviewer3Id>"; "Entitlement 9" = "<reviewer3Id>"
}

# 3. Group item IDs by reviewer
$byReviewer = @{}
foreach ($i in $items) {
  $name = $i.accessSummary.entitlement.name
  if ($assignments.ContainsKey($name)) {
    $rid = $assignments[$name]
    if (-not $byReviewer.ContainsKey($rid)) { $byReviewer[$rid] = @() }
    $byReviewer[$rid] += @{ id = $i.id; type = "ITEM" }
  }
}

# 4. Reassign in chunks of 50
foreach ($rid in $byReviewer.Keys) {
  $list = $byReviewer[$rid]
  for ($n = 0; $n -lt $list.Count; $n += 50) {
    $chunk = $list[$n..([Math]::Min($n + 49, $list.Count - 1))]
    $body = @{
      reassign   = $chunk
      reassignTo = $rid
      reason     = "Bulk reassignment via API"
    } | ConvertTo-Json -Depth 5

    Invoke-RestMethod -Method Post -Headers $hdr `
      -Uri "$base/certifications/v1/$certId/reassign" -Body $body
  }
}

just an FYI, This isn’t supported for certifications assigned to Governance Groups.

This is just my take, so if anyone has a better approach, feel free to add it here.

Thanks,

Suraj

Hi @Rosha,

Out of curiosity, what led you to reassign within one campaign rather than run three separate ones? A search campaign lets you set a single reviewer for all its certifications through searchCampaignInfo.reviewer (create campaign), so one campaign per reviewer might avoid the reassignment step altogether. There may well be a reason that doesn’t fit your case, and knowing it would help point you to the best option.

If you do need to reassign within an active campaign, @suraj_gorle’s flow is the right one. I would maybe consider a few things before running it on a real campaign:

  • access-review-items returns at most 250 items per call, so page with offset until a page comes back short (list access review items).
  • The same endpoint accepts an entitlements query parameter with comma-separated entitlement IDs. That narrows the response to items related to those entitlements, which replaces the name lookup. IDs are also safer than names, which can repeat across sources.
  • POST /certifications/{id}/reassign-async takes up to 500 items per call instead of 50, and returns a task you can check with GET /certification-tasks/{id}.
$base = "https://<tenant>.api.identitynow.com/v2026"
$hdr  = @{ Authorization = "Bearer <token>"; "Content-Type" = "application/json" }
$campaignId = "<campaign id>"

# Target reviewer identity ID -> comma-separated entitlement IDs
$plan = @{
  "<reviewer1Id>" = "<ent1Id>,<ent2Id>,<ent3Id>"
  "<reviewer2Id>" = "<ent4Id>,<ent5Id>,<ent6Id>"
  "<reviewer3Id>" = "<ent7Id>,<ent8Id>,<ent9Id>"
}

# Page this too if the campaign has more than 250 certifications
$f = [uri]::EscapeDataString("campaign.id eq `"$campaignId`"")
$certs = @(Invoke-RestMethod -Headers $hdr -Uri "$base/certifications?filters=$f&limit=250")

foreach ($cert in $certs) {
  foreach ($reviewerId in $plan.Keys) {
    $items = @(); $offset = 0
    do {
      $page = @(Invoke-RestMethod -Headers $hdr -Uri ("$base/certifications/$($cert.id)/access-review-items" +
        "?entitlements=$($plan[$reviewerId])&limit=250&offset=$offset"))
      $items += $page; $offset += 250
    } while ($page.Count -eq 250)

    $items = @($items | Where-Object { $_.accessSummary.access.type -eq "ENTITLEMENT" })

    for ($n = 0; $n -lt $items.Count; $n += 500) {
      $chunk = $items[$n..([Math]::Min($n + 499, $items.Count - 1))] |
        ForEach-Object { @{ id = $_.id; type = "ITEM" } }
      $body = @{ reassign = @($chunk); reassignTo = $reviewerId; reason = "Bulk reassignment" } |
        ConvertTo-Json -Depth 5
      $task = Invoke-RestMethod -Method Post -Headers $hdr -Body $body `
        -Uri "$base/certifications/$($cert.id)/reassign-async"
      Write-Host "$($cert.name) -> $reviewerId : task $($task.id) $($task.status)"
    }
  }
}

The caller needs ORG_ADMIN or CERT_ADMIN, or must be a reviewer on that certification. As Suraj noted, certifications assigned to a governance group are not supported by the reassign endpoint.

I would ask the same question as @Pratyush27, which is what is the reason for not having this split out into individual certifications?

I did something similar to set up an access owner certification for an app for Dev Days this year. In that case, some of the entitlements and access profiles did not have owners set, so they wanted those deferred to source owner or specified owner based on logic. This was before they had the source owner certification, and is still relevant if you need some custom logic for who the certifier should be.

Here is the presentation: Leveraging the APIs to build missing certifications