Platform Selector


Overview of Custom Categories

Foundry, EVERFI’s digital learning platform, enables each organization to define their own set of custom categories and labels and assign them to users. Custom categories are used most frequently to to assign specific training and educational content to learners.

A user may have at most one label in each category; think of category as a single-select property, not a multi-select property. For example, if you have a category called Department, with labels for Accounting, IT, Operations, Facilities and HR, a user could be in HR, or Accounting, but not both Accounting and HR. Or a user might have no label at all in this category.

The Foundry API offers three different ways to manage a user’s category_labels. Depending on the way you structure your API integration flow, you might employ one, two or all three techniques. None of these techniques is the best, or the worst, but each may have its place depending on your scenarios.

Read on for an overview of each technique, and also review the API technical documentation for detailed field-by-field reference documentation and sample requests and responses you can easily run in Postman.


What do Categories and Labels Look Like in the API?

What do Categories and Labels look like in the API?

Each category has a category ID, which is an integer. Within a category, there can exist 1 or more Labels. Each label has a Label ID, which is an integer. A user resource has a property called category_labels which is an array of all that user’s Label IDs. As noted earlier, each of these label IDs by definition will be in a different Category, because a user can have at most one Label per Category.

Here is a simple example of a catalog of categories and labels for an organization. Your own Foundry organization will of course have different values and IDs. We will use these values in the various case studies in this article.

Label ID Label Name Parent Category ID Parent Category Name
848 Accounting 4098 Department
987 HR 4098 Department
1254 IT 4098 Department
5467 Engineering 8798 Major
7897 French 8798 Major
3873 Anthropology 8798 Major
9878 Classics 8798 Major
2152 Hockey 10211 Athletic Team
3583 Basketball 10211 Athletic Team
248 Track and Field 10211 Athletic Team
456 Full time 568 Employment Status
789 Part time 568 Employment Status
12 Contract 568 Employment Status

 

If you were to GET a user, the response for a user who is in the Accounting Department (848 on line 28) and has an Engineering Major (5467 on line 24) would might like this:

{
    "data": {
        "id": "7893d56c-e58e-4065-a466-eea5958378d0",
        "type": "users",
        "attributes": {
            "active": true,
            "email": "first.last@stateu.edu",
            "first_name": "First",
            "last_name": "Last",
            "location_id": 13325,
            "sso_id": "first.last@stateu.edu",
            "student_id": "829298296",
            "user_rule_set_roles": [
                {
                    "rule_set": "he_learner",
                    "role": "graduate"
                }
            ]
        },
        "relationships": {
            "category_labels": {
                "data": [
                    {
                        "id": "5467",
                        "type": "category_labels"
                    },
                    {
                        "id": "848",
                        "type": "category_labels"
                    }
                ]
            }
        }
    },
}

If your organization had the categories and labels above, then if you sent this GET request to https://api.fifoundry.net/v1/admin/categories/?include=category_labels then the response would look like this:

{
  "data": [
    {
      "id": "4098",
      "type": "categories",
      "attributes": {
        "name": "Department"
      }
    },
    {
      "id": "8798",
      "type": "categories",
      "attributes": {
        "name": "Major"
      }
    },
    {
      "id": "10211",
      "type": "categories",
      "attributes": {
        "name": "Athletic Team"
      }
    },
    {
      "id": "568",
      "type": "categories",
      "attributes": {
        "name": "Employment Status"
      }
    }
  ],
  "included": [
    {
      "id": "848",
      "type": "category_labels",
      "attributes": {
        "category_id": 4098,
        "category_name": "Department",
        "name": "Accounting",
        "users_count": 17
      },
      "relationships": {
        "category": {
          "data": {
            "id": "4098",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "987",
      "type": "category_labels",
      "attributes": {
        "category_id": 4098,
        "category_name": "Department",
        "name": "HR",
        "users_count": 31
      },
      "relationships": {
        "category": {
          "data": {
            "id": "4098",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "1254",
      "type": "category_labels",
      "attributes": {
        "category_id": 4098,
        "category_name": "Department",
        "name": "IT",
        "users_count": 17
      },
      "relationships": {
        "category": {
          "data": {
            "id": "4098",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "64263",
      "type": "category_labels",
      "attributes": {
        "category_id": 8798,
        "category_name": "Major",
        "name": "Engineering",
        "users_count": 122
      },
      "relationships": {
        "category": {
          "data": {
            "id": "8798",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "7897",
      "type": "category_labels",
      "attributes": {
        "category_id": 8798,
        "category_name": "Major",
        "name": "French",
        "users_count": 19
      },
      "relationships": {
        "category": {
          "data": {
            "id": "8798",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "64249",
      "type": "category_labels",
      "attributes": {
        "category_id": 8798,
        "category_name": "Major",
        "name": "Anthropology",
        "users_count": 11
      },
      "relationships": {
        "category": {
          "data": {
            "id": "8798",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "64236",
      "type": "category_labels",
      "attributes": {
        "category_id": 8798,
        "category_name": "Major",
        "name": "Classics",
        "users_count": 4
      },
      "relationships": {
        "category": {
          "data": {
            "id": "8798",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "2152",
      "type": "category_labels",
      "attributes": {
        "category_id": 10211,
        "category_name": "Athletic Team",
        "name": "Hockey",
        "users_count": 43
      },
      "relationships": {
        "category": {
          "data": {
            "id": "10211",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "3583",
      "type": "category_labels",
      "attributes": {
        "category_id": 10211,
        "category_name": "Athletic Team",
        "name": "Basketball",
        "users_count": 23
      },
      "relationships": {
        "category": {
          "data": {
            "id": "10211",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "248",
      "type": "category_labels",
      "attributes": {
        "category_id": 10211,
        "category_name": "Athletic Team",
        "name": "Track and Field",
        "users_count": 27
      },
      "relationships": {
        "category": {
          "data": {
            "id": "10211",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "456",
      "type": "category_labels",
      "attributes": {
        "category_id": 568,
        "category_name": "Employment Status",
        "name": "Full time",
        "users_count": 244
      },
      "relationships": {
        "category": {
          "data": {
            "id": "568",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "789",
      "type": "category_labels",
      "attributes": {
        "category_id": 568,
        "category_name": "Employment Status",
        "name": "Part time",
        "users_count": 41
      },
      "relationships": {
        "category": {
          "data": {
            "id": "568",
            "type": "categories"
          }
        }
      }
    },
    {
      "id": "12",
      "type": "category_labels",
      "attributes": {
        "category_id": 568,
        "category_name": "Employment Status",
        "name": "Contract",
        "users_count": 17
      },
      "relationships": {
        "category": {
          "data": {
            "id": "568",
            "type": "categories"
          }
        }
      }
    }
  ],
  "links": {},
  "meta": {
    "total_count": 4
  }
}

Note the data array has 4 elements, each element being an object of a category, and the relationships array has 13 elements, 3 each for the labels in the Department, Athletic Team and Employment Status  categories and 4 for the labels in the Major category. The property meta.total_count is the count of categories which in this example is 4.


Option A: Set category_labels in a User POST or PATCH

This is the most direct and straightforward way to set a user’s categories. In a POST or PATCH request, in the request payload you simply provide an array of each Label ID you want the user to have. It is very important to note that if your API request sends category_labels, then whatever you provide will do a full overwrite of all the user’s labels. It is not additive — it replaces in full. So only send a user’s category_labels only if you can provide all of them. In other words, if a user currently has labels A, B and C, and you PATCH the user with B, P and Q, then after the PATCH, the user will have labels B, P and Q, and labels A and C are removed. If you omit the category_labels attribute entirely from the request payload, then Foundry will not change the user’s category_labels at all.

Sample request payload to POST a new user, including categories:

{
  "data": {
    "type": "registration_sets",
    "attributes": {
      "registrations": [
        {
          "rule_set": "user_rule_set",
          "first_name": "First",
          "last_name": "Last",
          "email": "myemail@stateu.edu",
          "location_id": 10415,
          "category_labels": [
            "848",
            "3873"
          ]
        },
        {
          "rule_set": "he_learner",
          "role": "undergrad"
        }
      ]
    }
  }
}

Next is a more complex scenario for updating a user’s categories in a PATCH request similar to the POST request above. Suppose a user starts out with the following categories and labels, using the sample values from the table above:

  • Department: Accounting (848)
  • Major: Engineering (5467)
  • Athletic Team: Hockey (2152)
  • Employment Status: none

Suppose you wanted to update this user to change to the following:

Category Before After
Department Accounting (848) HR (987)
Major Engineering (5467) Engineering (5467)
Athletic Team Hockey (2152) none
Employment Status none Full time (456)

As you can see in the table above, we are doing a mix of updates: changing one category’s label (Department), keeping one category the same (Major), dropping one category entirely (Athletic Team) and setting a label in a category that previously was empty (Employment Status).

For this PATCH request, set category_labels like this:

{"category_labels": ["987","5467","456"]

The complete PATCH request body:

{
  "data": {
    "type": "registration_sets",
    "id": "7893d56c-e58e-4065-a466-eea5958378d0",
    "attributes": {
      "registrations": [
        {
          "rule_set": "user_rule_set",
          "first_name": "First",
          "last_name": "Last",
          "email": "myemail@stateu.edu",
          "location_id": 10415,
          "category_labels": [
            "987",
            "5467",
            "456"
          ]
        }
      ]
    }
  }
}

Note that the order you put the label IDs in does not matter.

Pros
  • Straightforward and direct.
  • Good technique to set categories when adding a new user, since you’ll be setting other user properties at the same time.

Cons

  • If you want to set values for many users, then you need to do this one user at a time when a bulk operation might be more efficient or direct.
  • If you have an integration operation that only cares about labels, then you are increasing your scope and complexity because you have to concern yourself with all the other properties of a user as well.
  • You must set all the labels, not just one, which means you must be sure to run a GET on the user so you know all their current labels so you can properly update them.

Option B: Set a single label for multiple users in a single request

The API has a bulk label update endpoint called bulk_action_categories that requires a POST with a request payload with three simple properties: the category, the label, and an array of users who should get that label. You can send up to 500 users per request.

Example:

{
  "data": {
    "type": "bulk_action_categories",
    "attributes": {
      "user_ids": [
        "cb34dfd5-da2d-4754-bd1a-e2c9871b043c",
        "9c986147-a6aa-4a18-bc7c-c27a17c7bffa",
        "f35160d9-4689-4d2b-95e2-8e860ec02c4f",
        "3b01121d-7a99-440e-b5ed-89981cb37cf9",
        "2b3a5ab4-3902-472f-8ff9-ddfb799e683c",
        "7bde0409-fced-403c-b0a2-d65a7e056509"
      ],
      "category_label": "Anthropology",
      "category_id": "8798"
    }
  }
}

Pros

  • Easy and efficient way to mass-assign users to a single label in a category
  • Don’t need to worry about other categories, as you can focus on a single label per request
  • Ideal for an integration flow that simply needs to update categories but is not concerned with other user properties

Cons

  • Ideal for mass-updating, but not when granularly updating one user at a time
  • Can set only one label per request

Option C: Set a single label for a single user in a single request

The third option for setting a user’s category in the API is the admin/category_label_users endpoint. This endpoint allows you to add or delete a single label to a single user in a POST request. This option makes sense if your integration flow needs this sort of precise operation.

One drawback to this technique is that it requires several steps. Before you can POST a user’s category labels, you will need to delete any existing label the user might already have for that category. This requires you to perform a series of GETs to discover the users’ existing labels, then run DELETES as necessary, then run your POSTs.

For example, suppose you want to set labels for users in the Department category, as shown in the earlier examples.

Suppose you want to give label “HR” to three users.

First, you’ll need to discover who has each Label currently. You can do this with a GET request like this, supposing that HR’s Label id is 987:

admin/category_label_users/?filter[category_label_id]=987&page[number]=1&page[size]=10

Alternately, you can discover this information for a single user, for example this GET request:

admin/category_label_users/?filter[user_id]=cb34dfd5-da2d-4754-bd1a-e2c9871b043c

The response to either of these requests returns an array of the following information shown below. If you filtered on category_label_id then the response would contain any users who have that label, and if you filtered on user_id then the response would have all the labels for that user.

{
    "data": [
        {
            "id": "27853",
            "type": "category_label_users",
            "attributes": {
                "category_label_id": 987,
                "user_id": "cb34dfd5-da2d-4754-bd1a-e2c9871b043c"
            },
            "relationships": {
                "category_label": {
                    "data": {
                        "id": "987",
                        "type": "category_labels"
                    }
                },
                "user": {
                    "data": {
                        "id": "cb34dfd5-da2d-4754-bd1a-e2c9871b043c",
                        "type": "users"
                    }
                }
            }
        },
        {
            "id": "6194884",
            "type": "category_label_users",
            "attributes": {
                "category_label_id": 3873,
                "user_id": "cb34dfd5-da2d-4754-bd1a-e2c9871b043c"
            },
            "relationships": {
                "category_label": {
                    "data": {
                        "id": "3873",
                        "type": "category_labels"
                    }
                },
                "user": {
                    "data": {
                        "id": "cb34dfd5-da2d-4754-bd1a-e2c9871b043c",
                        "type": "users"
                    }
                }
            }
        }
    ]
}

The id property (27853 and 6194884 in the above example response) is the id of the category_label_users which is the resource that is the union of one user and one label. If you need to DELETE that category_label_users to replace it with a different label within the same category for the same user, you delete for that id, 27853 in this example, with a DELETE by id request.

If any of these users currently have the other Labels in that category (Accounting or IT), then you’ll need to DELETE their category_label_users one at a time, to clear the way to add the HR Label. If the user already has the HR label, or has no label at all in the category, then you can skip the DELETE step for that user because there’s nothing to delete.

Next, you will send a POST to category_label_users for each user for the HR Label (supposing its label id is 987 in the example) with a request providing the user_id and the Label ID in the category_label_id property like this:

{
  "data": {
    "type": "category_label_users",
    "attributes": {
      "user_id": "f5d82e9e-f4b4-4a95-9edd-2d3b485ce86c",
      "category_label_id": "987"
    }
  }
}

Depending on your integration flow, you can do these operations for many users at once, or one user at a time. If you find that you are doing this for many users at once, then Option B bulk_action_categories might be a better route for you.

Pros

  • If your integration flow calls for a granular update of one category for one user, this might be the right technique.
  • You can manage a user’s labels without needing to change the actual user itself, compared to Option A.

Cons

  • If changing a user’s category from one label to another, must first delete existing category_label_user before adding the new one.
  • Could require many requests to get the job done, which could take longer to run and possibly exceed API rate limits.

Retrieving Categories and Labels in the API

Once configured initially, and perhaps at period intervals thereafter, the categories and labels themselves rarely change in Foundry. Therefore, to optimize your API integration, retrieve the categories and labels sparingly and cache them in your application. It is strongly recommended that you not retrieve all the categories and labels in every series of API calls.

For example, if you run a daily one-time synch every 24 hours, then it is fine to retrieve the list of Foundry categories and labels one time at the beginning of that synch. But if you have a routine that sets users labels all throughout the day, don’t retrieve the categories and labels from the API for each and every user update. Instead, consider adding a way to retrieve these values at a regular interval (daily or even hourly is fine), storing the results somewhere accessible in your integration layer, and using the cached values.

Note that this section refers to the actual categories and labels themselves, not to which labels users have. It is common for users to have various labels change as updates are made to users in the system of record.

If you have many categories and label, then we recommend you break these GET requests down the following way:

  1. First, get all the categories without the labels with a GET request to admin/categories
  2. Next, iterate through the response and send a GET request to GET each category returned in the response, and include its labels, for example: admin/categories/4098?include=category_labels and then admin/categories/8798?include=category_labels

Managing Categories and Labels in the API

If your API integration requires you to add a new Label to an existing Category, this is possible. For example, looking at the example of the Department category above, your integration might be required to add a new Label called Talent. The Foundry API allows you to add new Labels and update the name of an existing Label. It is not possible to move a Label from one Category or another, or to delete a Label.

Examples:


Mapping Foundry Labels into your System

A common API integration task is coming up with a way to associate categories and labels in Foundry with their counterparts in your system of record. While categories and labels are a simple two-dimensional construct in Foundry, it’s possible that in your system of record, what makes a user have a particular label in a particular category can have complex logic and different categories may have their origins in a variety of systems. Ultimately, your API integration needs to coalesce these data sources into the Foundry categories and labels.

In working with many EVERFI clients, a best practice has emerged to have mapping tables or similar resources located somewhere in the integration platform that map Foundry categories and labels to the counterpart values in your systems. If there are only a handful of labels in a category (where the values are relatively limited as opposed to a more flexible list that may change over time), then perhaps you need only a switch/case statement instead of a more elaborate table or array for a longer list of values. For shorthand, we’ll call this a “mapping resource.” Foundry categories and labels have only 2 properties each: the backend integer id which is assigned by Foundry when the record is added, and the visible name. Therefore, if you need to have any other means to classify or identify a label to map it to its source information in your systems, that will need to exist in your mapping resource.

Generally, the Foundry API requires you to work with label and category id (with one exception – the bulk_action_categories route needs the label name). So for any series of API requests, you’ll need to have the list of categories and labels with their name and id values

For example, suppose you have a student registration system and you have a category for department major. If you name the Foundry labels the same as the major names, which is highly recommended to avoid ambiguity, then your integration layer can search in the mapping resource for a label within the Department Major category where the label name is the same, and use the category and label id from there when getting and setting values in the API.

In some cases, you might have a simpler and smaller set of labels in Foundry that relate to more complex source data in your system of record. In this case, you’ll need to develop more complex ways to map this domain logic to the Foundry categories and labels.