Best Of
Some sample code to use the SKY API in Visual Basic
I write all of the programs that access the SKY API in Visual Basic. I use the SKYLib•Net library from Protege Solutions. It handles a lot of the ugly details so that I can focus on the data. You can download the library and an example program at the Protege website. https://protege.com.au/skylib
The download is free and will work with your Blackbaud database. You pay for the library only if you find it useful to you. It runs in a rate-limited mode until you buy a license. (This is not a sponsored post. I'm just a satisfied user of that software.)
The code sample below will give you an idea of what accessing the SIS data using the SKYLib library looks like.
Make the connection to the SIS database:
Private Sub ConnectToBbSKY(Optional ByVal UseBasicAuth As Boolean = True) Try
' Initialize the API and fill the call selector according to the selected Blackbaud product.
Api.SubscriptionKeyPri = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "Primary")
Api.SubscriptionKeySec = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "Secondary")
Api.RedirectUri = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "Redirect")
Api.ClientId = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "AppID")
If UseBasicAuth Then
Api.BasicAuthCredential = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "BASICAuth")
Else
Api.ClientSecret = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "AppSecret")
End If
Dim SKYLIB_KEY As String = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "Key")
' Manual user authorization can be disabled for unattended applications such as Windows Services.
Api.DisableUserAuth = False
Dim connected As Boolean = AppLicense.Init(LicenseKey:=SKYLIB_KEY)
If Not connected Then
MyLogger.Log(AppLicense.ErrMsg)
If UseBasicAuth Then
' we tried with the Basic Auth key and failed. Try the App Credential
MyLogger.LogError("BasicAuth failed. Using Client/Application Secret.")
Api.ClientSecret = SsesLibrary.SsesLibrary.GetPasswordFor("SKYLib", "AppSecret")
Api.BasicAuthCredential = ""
connected = AppLicense.Init(LicenseKey:=SKYLIB_KEY)
End If
If Not connected Then
MyLogger.LogError("Client/Application Secret failed. (1)")
Return
End If
End If
' The license expiry date, type and state are available for reference.
If AppLicense.Valid Then
MyLogger.Log(AppLicense.State)
MyLogger.Log("The SKYLib.NET license type is: '" + AppLicense.Type + "'.")
MyLogger.Log("Registration code: " + SKYLib.AppLicense.RegistrationCode)
Api.MaxRetries = 4
Api.RetryInterval = {15, 60, 300} ' 1st: 10 sec, 2nd: 1 min, 3rd: 5 min, 4th: 5 min (last value is used for subsequent intervals)
Api.ThrowErrorOn.CallQuotaExceeded = True
Api.ThrowErrorOn.ServerOutage = False
Else
MyLogger.LogError("SKYLib.NET is running in rate-limited mode. Need a valid license")
End If
If (Api.CheckVersion() AndAlso Api.LatestVersion > Api.CurrentVersion) Then
MyLogger.LogWarning("A new version of the SKYLib.NET library is available. The new version is " +
Api.LatestVersion.ToString() + ".")
End If
If Api.School.TestConnection() Then
MyLogger.Log("Connection to BBSky (School API) succeeded")
Else
MyLogger.LogError("Connection to the School API failed")
End If
Catch x As Exception
MyLogger.LogError(x.Message)
Finally
End Try End Sub
(Most of the code above for connecting to the database came from the sample program that comes with the SKYLib library download.)
Read user data via the UserExtendedGet endpoint:
Private Sub GetStudentInfo(roleIDList As String)
Dim moreToDo As Boolean
Dim nextRec As Integer = 1
Dim dStartTime As Date = DateTime.Now
Try
Do
Dim usersList As School.Model.UserExtendedCollection =
Api.School.V1UsersExtendedGet(roleIDList, marker:=nextRec)
For Each user As School.Model.UserExtended In usersList.Value
If Not user.Deceased Then
StudentsCoreByUserID.Add(user.Id.ToString, user)
If String.IsNullOrEmpty(user.NickName) Then
MyLogger.LogWarning("No nickname in Core for " + user.FirstName + " " +
user.LastName + " - " + user.Id.ToString)
End If
End If
Next
nextRec = ParseNextLink(usersList.NextLink)
moreToDo = nextRec > 0
Loop While moreToDo
Catch ex As Exception
MyLogger.LogError(ex.ToString)
End Try
MyLogger.Log("GetCoreData(): Read Core data for " + StudentsCoreByUserID.Count.ToString() + " students")
Return
End Sub
Read an Advanced List via the ListSingle endpoint. This is a generic function that will read any list and return the data to me.
Private Function GetListData(listID As Integer, label As String) As List(Of Dictionary(Of String, School.Model.Field))
Dim result As New List(Of Dictionary(Of String, School.Model.Field))
MyLogger.Log(String.Format("GetListData({0}): {1}", listID, label))
Dim page As Integer = 1
Dim done As Boolean = False
Try
While Not done
Dim list As School.Model.ListResult = Api.School.V1ListsAdvancedByListIdGet(listID, page)
Dim rows As List(Of School.Model.Row) = list.Results.Rows
For Each row As School.Model.Row In rows
Dim fieldData As Dictionary(Of String, School.Model.Field) = GetFieldData(row)
result.Add(fieldData)
Next
done = (rows.Count = 0)
page += 1
End While
MyLogger.Log("Record count = " + result.Count.ToString)
Return result
Catch ex As Exception
MyLogger.LogError("GetListData(): " + ex.ToString)
Return Nothing
End Try
End Function
Private Function GetFieldData(aRow As School.Model.Row) As Dictionary(Of String, School.Model.Field)
Dim fieldData As Dictionary(Of String, School.Model.Field)
fieldData = aRow.Columns.ToDictionary(Function(c) c.Name)
Return fieldData
End Function
An example of how I use my GetListData function (above) to process the data
Private Sub GetFakeAndAlt()
FakeAndAlt = New HashSet(Of String) Dim CoreID As Integer
Dim Status As String = ""
Try
' Advanced List - (API Acccess) API - Special Student Enrollment
Dim listData As List(Of Dictionary(Of String, School.Model.Field)) = GetListData(117174, "API - Special Student Enrollment")
listData.ForEach(
Sub(row)
CoreID = row("UserID").Value()
If Not IgnorableUserIDs.Contains(CoreID) Then
Status = row("Enrollment").Value()
If Status IsNot Nothing Then
Status = Status.ToLower.Trim
If Status.Equals("fake student") Or
Status.Equals("alternate study") Or
Status.Equals("medical leave") Or
Status.Equals("academic incomplete") Then
FakeAndAlt.Add(CoreID.ToString)
End If
End If
End If
End Sub)
Catch ex As Exception
MyLogger.LogError("GetFakeAndAlt(): " + NL + ex.ToString)
End Try End Sub
Re: How to handle recurring gifts set up during appeal
We briefly tried moving active recurring gifts to a more general appeal after a year and, fwiw, hated that.
We do have annualized campaigns, so I will update the campaign to the FY27 one in a week, but the method for getting the gift—the appeal—stays.
New Opportunity Webhook Event Types for Webhook API
To enable integrations to respond to opportunity lifecycle changes, we added the following webhook event types for Raiser's Edge NXT opportunities:
com.blackbaud.opportunity.add.v1— Fires when an opportunity is added.com.blackbaud.opportunity.change.v1— Fires when an opportunity is changed.com.blackbaud.opportunity.delete.v1— Fires when an opportunity is deleted.
For details, see Opportunity webhook event types.
Re: 🔥 Ask the All-Stars: Real-World APIs with Brian Gray & Lauren Henderson (June 24–26)
Hi everyone! I have been learning about and using the blackbaud APIs since 2021. I've used it to pull out an advanced list into Power Automate for scheduled notification emails (which was recognized at BBCON in 2025 with a Silo Buster Impact Award!)
Re: 🔥 Ask the All-Stars: Real-World APIs with Brian Gray & Lauren Henderson (June 24–26)
I have been using Blackbaud APIs since 2004 - starting with the Education Edge API and then moving on to the SKY API when we moved to the SIS/LMS in 2020.
I write programs using Microsoft’s Visual Studio (because that was the only option available to us for the old EE API in 2004). I use the SKYLib Net library from Protege Solutions to actually do the authentication and make the data calls. (I could write programs without the Protege library, but then I would have to actually learn how the OAuth authentication flow is supposed to work. SKYLib handles that for me.)
Blackbaud has a good set of resources to help you get started with the API.
1) Create an account and connect it to your school’s database (link: )
2) Read documentation about specific API endpoints to actually get data. A good one to start with is the User Extended endpoint (link: ). (An "endpoint" is just a URL that requests a specific action by the API - Create a new record, Read one or more records, Update an existing record, or Delete a record.)
Read about the endpoint, then click the Try It button and enter your UserID to retrieve your own data.
Re: Introducing Online Constituent Forms: Streamline Data Collection for New and Existing Constituents
These look great, but we will not use them until they have at least business information and flexibility about which phone type is updated, because we want to get URLs from our alumni. And the ability to update as many "phone numbers/types" as we would like.
We also have space for people to enter different names - the name they used when they went to college or a new name they are currently using. I don't think that capability exists for these forms yet.
Re: What's New In Blackbaud Raiser's Edge NXT® — June 9, 2026
We have been having data trouble with webview posting to FE and Anthony and team has been fixing bug that caused it.
I would be careful using this and really look at the entries in RE and FE when you start using it to ensure it is what you intend. (error like out of balance GL JE batch, different amount in credit vs debit, and mismatch amount from the RE post batch).
Re: Packages in Blackbaud Raiser's Edge NXT®: What They Are, How to Use Them, and What's Coming Next
@Bill Connors -
Thank you so much for the kind words, and for this thoughtful feedback — this is exactly the kind of conversation I was hoping to spark.
You're right that the database view has had Package columns in those grids for a while, and I completely understand the frustration with the gap in the web experience. Here's some good news on that front: all customers will see the following Package columns in the Package list next week:
- Cost
- Total revenue
- Gift count
- Donors count
- Largest gift
That covers the core "how did each Package perform?" question you're describing — gifts, dollars, and enough context to evaluate basic package performance directly in the web view.
The additional analytical columns — average gift, median gift, ROI, and conversion rate — will be part of the premium features. The thinking behind that split is that those metrics move from reporting what happened into helping teams decide what to do next: whether this package was worth the cost, how it benchmarks, where to invest next cycle. That decision-support layer is where we've focused the premium feature set.
On the goal performance view (over/under, percent of goal) — we're introducing a new goal system across the board, and Package-level goal analysis is something we'll be building on top of that foundation in coming releases. Stay tuned for more on that.
Re: Packages in Blackbaud Raiser's Edge NXT®: What They Are, How to Use Them, and What's Coming Next
@Heather McLean Can you explain the design intent of having a Package Tile that appears on the Details tab and there is no Packages tab, you enter one or more packages on the Packages tile and THEN a Packages tab show ups, and the tile and the tab then just duplicate functionality after that? This seems like unusual behavior — is it intentional? Thanks.
FYI that this makes your link to the Help file confusing because it talks about a Packages tab which doesn't exist until a package is added to the Packages tile on the Details tab.
Re: Packages in Blackbaud Raiser's Edge NXT®: What They Are, How to Use Them, and What's Coming Next
@Bill Connors -
Great question — the behavior is intentional, and here's the design logic behind it:
The Packages tile on the Details tab is where you get started — it's your starting point for adding packages to an appeal record. Once you have at least one package, the Packages tab appears, giving you a dedicated list view where you can also analyze, add, edit, or remove packages. We intentionally hide the tab until there's something there — an empty Packages tab on every appeal would just be noise for customers who don't use packages.
If you have access to premium features, there's also a separate Packages tile on the Analysis tab with comparative performance metrics.
On the help file — if you look at the Appeal Records topic, the Packages link in the navigation actually includes the note "(Only appears when at least one package exists)", so it does document the conditional behavior. That said, I can see how landing on the help article before adding a package and seeing a tab referenced that isn't there yet could be disorienting. We're always looking to make our help content clearer — if anything feels confusing, please use the feedback option on the help page and let us know.






