SAS Viya supports REST API that can help accomplish various tasks from data management to model scoring.
As of the Stable 2025.04 release, you can also create and manipulate SAS Visual Analytics reports using REST API. REST API make it:
In this post, we will learn:
You can learn more about SAS Viya REST API on the SAS developers site. Ensure you select the cadence corresponding to the SAS Viya version you have available, and filter for Visualization and Reporting:
You will notice there are several REST APIs in this category:
In this post, I will largely focus on the Visual Analytics API. Click on the Visual Analytics API to see all its endpoints listed by category. In this post, we focus on the endpoints in the Report category.
There are various tabs at the top. I recommend reading the Details tab the first time you use a new API. You won’t need to read through the documentation for Authentication, as long as you submit the code from SAS Studio Flows or if you have already set up authentication to SAS using the SAS extension in Visual Studio Code.
Navigate to the Reports category and click on Create a report while applying the specified operation(s):
Note that this endpoint is a POST request (we will come back to this later). Other requests are GET and PUT. You can learn more about this in the Details tab of the API (highlighted earlier). You can access SAS Viya REST API using Shell, JavaScript, Node, Python, Go, C, C#, Java, HTTP, Kotlin, R and Ruby, as well as SAS’ own PROC HTTP (see post and this YouTube video). In this post, we will focus on Python, using the Requests library. You can also use pure Python 3.
Let’s get started. First let’s add the data we will use to build our report to CAS. From your preferred editor, either SAS Studio or Visual Studio Code, paste the following code. Note you can also find all the code from this post in this notebook. We load the CLASS table from the SASHELP library into CAS to use it in a report:
cas;
proc casutil;
droptable casdata="class" incaslib="casuser" quiet;
load data=sashelp.class outcaslib="casuser" casout="class" promote;
quit;
The next step is to put the authentication token and the SAS Viya host URL into macro variables. These are then read into Python variables in the PROC PYTHON and are referenced in each API call. This is the easiest way to authenticate. You will need to rerun this code if your session times out.
%let token=%sysget(SAS_CLIENT_TOKEN);
%let host=%sysfunc(getoption(SERVICESBASEURL));
proc python;
submit;
import pandas as pd
import requests
token = SAS.symget('token')
host = SAS.symget('host')
endsubmit;
run;
We want to create the report and add a data source at the same time. At the top right, select the addData operation.
Note that the required structure of the request body with all its options is documented further below. Let us study the structure of the request sample code provided – most requests will follow this structure. The code contains:
The report body is a Python dictionary, where each parameter is a key and its value is the value in the dictionary. Note the options for resultNameConflict: in case of a naming conflict, you could also choose to abort creating the report, or add a numeric suffix to the report name. Note that operations needs to be an array. Click on operations and select addDataOperationRequest in the drop-down to see the requirements for the addData operation.
Note the structure of the response body: we need to specify the server, library and table name of the data source in a dictionary called cas, in a nested dictionary as the value to the key addData, which is an element of the operations array. This is how you can create the necessary code from the documentation - each time we drill in (as shown above), we need to use nested dictionaries in the code. This is a key point in this post. Understanding this will allow you to use any API endpoint based on the documentation.
Our sample code is almost ready to use. Lastly, we will wrap it in a PROC PYTHON submit endsubmit block, and include our CAS table, URL host and the authentication token. We will need to adapt the sample code from the documentation more in later examples.
proc python;
submit;
reportName = "Report123"
url = str(host) + "/visualAnalytics/reports"
payload = {
"resultFolder": "/folders/folders/@myFolder",
"resultReportName": reportName,
"resultNameConflict": "replace",
"operations": [{ "addData": { "cas": {
"server": "cas-shared-default",
"library": "CASUSER",
"table": "CLASS"
} } }
]
}
headers = { "Content-Type": "application/json",
"Authorization": "Bearer " + token,
"Accept": "application/json, application/vnd.sas.report.operations.results+json, application/vnd.sas.report.operations.error+json, application/vnd.sas.error+json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
endsubmit;
run;
When you execute the code, the response is printed;
{'resultReportId': 'ce648f31-8cb5-4e8c-9666-57dbff5689ba', 'resultReportName': 'Report123',
'resultReportUri':'/reports/reports/ce648f31-8cb5-4e8c-9666-57dbff5689ba',
'resultFolderUri':'/folders/folders/@myFolder',
'operations': [{'name':'ds7', 'label': 'CLASS', 'status': 'Success'}], 'status': 'Success', 'links': [{'method': 'GET',
'rel': 'getReport', 'href': '/reports/reports/ce648f31-8cb5-4e8c-9666-57dbff5689ba', 'uri': '/reports/reports/ce648f31-8cb5-4e8c-9666-57dbff5689ba', 'type': 'application/vnd.sas.report+json'}]}
The expected response is also documented:
In a section further below, we will need to extract a specific element from the response. To achieve this, we need to understand the structure of the response, which is what the Response Sample in the documentation is for. This response sample is for response code 201, which indicates a successful API call. These codes are standardized, and the most common responses for each endpoint are documented:
The response contains the report ID of the created report. We will need this report ID to update the report. By understanding the structure of the response, we can figure out how to extract it. In this case, it is not nested and we can directly extract as follows:
proc python;
submit;
reportID = response.json()['resultReportId']
print("Report ID: " + reportID)
endsubmit;
run;
Before we move on to updating the report, we need to understand what ETags are. When updating content, SAS Viya has a built-in safety mechanism to prevent unwanted overwrites of a resource. Specifically, to prevent so called mid-air collisions: two people writing different changes to the same resource at the same time. This is done with the ETag (entity tag).
The ETag is a HTTP standard and identifies a specific version of a resource. Therefore, each time we want to update a report, we need to include its most recent ETag in the request. We can retrieve the most recent ETag using the retrieve_etag function, which relies on the getReport endpoint in the Reports REST API (mentioned earlier):
proc python;
submit;
def retrieve_etag(reportID):
reportURI = 'reports/reports/' + reportID
url = str(host) + '/' + reportURI
print(url)
headers = {"Authorization": "Bearer " + token,
"Accept" : "application/json"}
response = requests.get(url, headers=headers)
return response.headers['ETag']
endsubmit;
run;
We put this into a Python function so that we can easily call it every time we want to update the report. The function just requires the report ID. You will need to rerun that block of code to redefine the function when the session expires.
Using the ETag, we can now update the report. We need to include the ETag in the request header, with the If-Match key.
We can apply various operations using the updateReport endpoint. Recall you can include multiple operations in a request.
Operation | Description |
addDataOperationRequest | Operation to add a data source to a report. Allows you to filter by data items and indicate properties for each data item (name, format, aggregation, classification and geographic context). |
updateDataOperationRequest | Operation to update an existing data source in a report. Allows you to change data item properties (name, format, aggregation, classification and geographic context). |
changeDataOperationRequest | Operation to replace a data source used in a report with another. This includes a facility for data item mapping if data items in replacement data source have different names. |
applyDataViewOperationRequest | Operation to apply a data view to a report. |
addPageOperationRequest | Operation to add a page to a report. |
addObjectOperationRequest | Operation to add an object to a report. |
updateObjectOperationRequest | Operation to update an object on a report. |
setParameterValueOperationRequest | Operation to set a parameter value on a report. |
In this section, we change the aggregation of a data item from sum to average. Note that as of Stable 2025.04, you always need to specify the classification of the data item (in this case: measure), even if it doesn’t change.
proc python;
submit;
ETag = retrieve_etag(reportID)
url = str(host) + "/visualAnalytics/reports/" + reportID
payload = {
"version": 1,
"resultFolder": "/folders/folders/@myFolder",
"resultReportName": "Report123",
"resultNameConflict": "replace",
"operations": [{ "updateData": {
"data": { "name": "CLASS" },
"dataItems": [
{
"dataItem": "Height",
"properties": {
"aggregation": "average",
"classification" : "measure"
}
}
]
} }]
}
headers = {
"If-Match": ETag,
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
"Accept": "application/json, application/vnd.sas.report.operations.results+json, application/vnd.sas.report.operations.error+json, application/vnd.sas.error+json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.json())
endsubmit;
run;
Before adding an object, we will check whether that object is already in the report. If you run the addObject operation in the updateReport API endpoint, it doesn’t check whether the object already exists. To avoid having the same bar chart twice, we can use the Get report content elements endpoint in the Reports API. It returns the contents of the report, including all objects. We can check whether an object with a specific name (in our case Bar – Sex 1) is already in the report:
proc python;
submit;
url = str(host) + "/reports/reports/" + reportID + "/content/elements"
headers = {
"Accept-Item": "application/vnd.sas.report.content.element+json",
"Authorization": "Bearer " + token,
"Accept": "application/json, application/vnd.sas.collection+json, application/vnd.sas.error+json"
}
response = requests.get(url, headers=headers)
if "Bar - Sex 1" in response.text:
print("The object 'Bar - Sex 1' already exists. We don't need to add it.")
else:
print("The object 'Bar - Sex 1' does not exist. Execute the next cell to add it.")
endsubmit;
run;
Note we are simply parsing the report content for the object name. This response works as long as the object name is not the same as something else in the report definition. In this case, we just print out whether the object already exists or not. Ideally, you would add the code from the following section into the else block of the code.
Finally, we can add objects to the report. Note the updateReport endpoint accepts either a reportObject or an object key in the request body. The reportObject key is meant to add an existing object from another report. Use the object key to add a completely new object. We can decide on how to position each object:
Position | Description |
reportPlacement | Allows placement in the start or the end of canvas or the header of a new page. |
pagePlacement | Allows placement in the start or the end of canvas or the header of an existing page. |
containerPlacement | Allows placement at the start or the end of an existing container. |
relativeToObjectPlacement | Allows placement before, after, to the left, right, on top, or on the bottom of an existing object. |
In the following code, we add a bar chart and a text object.
proc python;
submit;
import requests
ETag = retrieve_etag(reportID)
url = str(host) + "/visualAnalytics/reports/" + reportID
payload = {
"version": 1,
"resultFolder": "/folders/folders/@myFolder",
"resultReportName": "Report123",
"resultNameConflict": "replace",
"operations": [
{
"operationId": "barChart",
"includeObjectInResponse": True,
"addObject": {
"object": {"barChart": {
"dataSource": "CLASS",
"dataRoles": {
"category": "Sex",
"measures": ["Frequency"]
}
} },
"placement": { "page": {
"target": "Page 1",
"position": "start"
} }
}
},
{
"operationId": "text",
"includeObjectInResponse": True,
"addObject": {
"object": {"text": {
"options": {
"content": "This bar chart shows the frequency of each Sex."
}
} },
"placement": {
"relativeToObject": {
"target": "Bar - Sex 1",
"position": "top"
}
} }
}
]
}
headers = {
"If-Match": ETag,
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
"Accept": "application/json, application/vnd.sas.report.operations.results+json, application/vnd.sas.report.operations.error+json, application/vnd.sas.error+json"
}
response = requests.put(url, json=payload, headers=headers)
print("Response Code: " + str(response.status_code))
print(response.json())
endsubmit;
run;
As you can see, most requests follow a similar structure. Once you understand how to navigate the documentation, you can make the necessary adjustments to the code. There is even more you can do with REST API. This will be covered in future posts.
Find more articles from SAS Global Enablement and Learning here.
The rapid growth of AI technologies is driving an AI skills gap and demand for AI talent. Ready to grow your AI literacy? SAS offers free ways to get started for beginners, business leaders, and analytics professionals of all skill levels. Your future self will thank you.