Using Profiles API after_id parameter

A while ago, a new parameter was added to the NERM Profiles API that allows for a very fast GET of all profiles in a tenant or for a particular Profile Type. In this post, I wanted to provide some code examples for how one can utilize this effectively

Relevant API docs: get-profiles | SailPoint Developer Community

When sending the initial request, you can leave the parameter blank. Having it present will tell our system to use that endpoint and with a limit of 500, we generally see responses in under 1s.

The response will look pretty much like any other Profiles API response but with two key differences:

  1. We include an “Archived” flag as using this parameter will return all profiles, archived and not. This allows you to filter out - or only keep - archived profiles.
  2. The _metadata object will include the next ID to use as an offset
{
    "profiles": [
        {
            "id": "000c1ce2-7073-460b-a1b7-f146ef907afb",
            "uid": "fec66e24cc89496fa3729757275562c1",
            "name": "Test 137",
            "profile_type_id": "047f1d6f-a231-4a7d-808f-35622794dbad",
            "status": "Active",
            "id_proofing_status": "pending",
            "archived": false,
            "updated_at": "2024-12-05T14:57:11.420-05:00",
            "created_at": "2024-08-16T11:31:46.271-04:00",
            "attributes": {
                "first_name": "Test 137"
            }
        }
    ],
    "_metadata": {
        "limit": 1,
        "total": 649,
        "next": "/profiles?after_id=000c1ce2-7073-460b-a1b7-f146ef907afb&limit=1",
        "after_id": "000c1ce2-7073-460b-a1b7-f146ef907afb"
    }
}

So, the pseudocode for using after_id would be:

  1. In a loop, send a GET request for /profiles that includes:
    1. A limit of 500
    2. The metadata parameter
    3. The after_id parameter
    4. (Optional) The Profile Type ID parameter
  2. Parse the response, storing the Profiles and tracking the next after_id value
  3. Continue looping until there are no more profiles to get
    1. You can do this by checking for a status code other than 200 or tracking the number of total profiles available against how many you have collected so far

Ruby - Here, I loop until I see a non-200 response. When that happens, I break the loop. Inside the loop, I make my GET request for Profiles. First, I pass in a blank string for after_id - but for every request after that, I use the after_id value from the metadata:

require 'json'
require 'net/http'
require 'uri'
require 'net/https'

def make_request(after_id)
  profile_type_id= "12345678-1234-1234-1234-123456789012"

  url = URI("https://acmeco.nonemployee.com/api/profiles?metadata=true&limit=500&profile_type_id=#{profile_type_id}&after_id=#{after_id}")

  https = Net::HTTP.new(url.host, url.port)
  https.use_ssl = true

  request = Net::HTTP::Get.new(url)
  request["Accept"] = "application/json"
  request["Authorization"] = "Bearer <TOKEN>"

  response = https.request(request)

  return response.read_body, response.code
end

profiles = Array.new
after_id = ""

loop do
  response_body,response_code=make_request(after_id)
  
  break if response_code != '200'

  after_id = JSON.parse(response_body)["_metadata"]["after_id"]
  JSON.parse(response_body)["profiles"].each do |profile|
    profiles << profile
  end
end

puts profiles

Go - Similar to the Ruby code, but golang is a bit more verbose:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
)

type ProfileResponse struct {
	Profiles []struct {
		ID               string            `json:"id"`
		UID              string            `json:"uid"`
		Name             string            `json:"name"`
		ProfileTypeID    string            `json:"profile_type_id"`
		Status           string            `json:"status"`
		IDProofingStatus string            `json:"id_proofing_status"`
		Archived         bool              `json:"archived"`
		UpdatedAt        string            `json:"updated_at"`
		CreatedAt        string            `json:"created_at"`
		Attributes       map[string]string `json:"attributes"`
	} `json:"profiles"`
}

type ResponseMetaData struct {
	Metadata struct {
		Limit   int    `json:"limit"`
		Offset  int    `json:"offset"`
		Total   int    `json:"total"`
		Next    string `json:"next"`
		AfterID string `json:"after_id"`
	} `json:"_metadata"`
}

func CheckError(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func MakeGetRequest(params string) ([]byte,int) {

	url := "https://acmeco.nonemployee.com/api/profiles?"+ params

	req, err := http.NewRequest(http.MethodGet, url, nil)
	CheckError(err)

	req.Header.Add("Authorization", "Bearer <TOKEN>")
	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)
	CheckError(err)

	respBody, err := io.ReadAll(resp.Body)
	CheckError(err)
	defer resp.Body.Close()

	return respBody, resp.StatusCode
}

func main() {
	var profiles ProfileResponse

	params := url.Values{}
	params.Add("metadata", "true")
	params.Add("profile_type_id", "12345678-1234-1234-1234-123456789012")
	params.Add("limit", "500")
	params.Add("after_id", "")
	
	for {
		var api_response ProfileResponse
		var respMetaData ResponseMetaData

		resp, status_code := MakeGetRequest(params.Encode())

		if status_code != 200{ break }

		CheckError(json.Unmarshal(resp, &api_response))
		CheckError(json.Unmarshal(resp, &respMetaData))

		for _, rec := range api_response.Profiles {
			profiles.Profiles = append(profiles.Profiles, rec)
		}

		params.Add("after_id", respMetaData.Metadata.AfterID)	
	}
	fmt.Println("Profiles\n",profiles)
}
1 Like