Skip to content

REST API

REST (REpresentational State Transfer): language-agnostic web-standards based constrained service principles.
API (Application Programmable Interface): a means to interact with or control an application or service.

A REST API in general terms is a set of rules and conventions for building and interacting with web services. It is based on the principles of REST, which include using standard HTTP methods (e.g. GET, POST, PATCH, DELETE, PUT) and following a stateless client-server architecture, amongst other constraints. REST APIs are commonly used to enable communication between different systems over the internet. While some APIs are RESTful, not all APIs follow the REST architecture. And while REST APIs are commonly used, there are other types of APIs, such as SOAP (Simple Object Access Protocol) and GraphQL.

Luckily, the Python community has developed mature and easy-to-use libraries for working with REST APIs. The most popular library is requests - a library that sees around 30M downloads per month. A modern drop-in replacement version niquests also exists and supports more advanced fceatures like HTTP2/3 along with multiplexing and concurrency if performance is a priority. These libraries provide a simple and intuitive way to interact with RESTful services, making it easy to send HTTP requests, handle responses, and work with JSON data. Other modern alternatives include httpx and aiohttp (for asynchronous programming).

Endpoints

Endpoints are specific URLs where API services are accessed. Each endpoint represents a resource or a collection of resources. For example, https://httpbin.org/get is an endpoint that returns data using the GET method.

Endpoints consists of the following parts:

  • Base URL: The base URL of the API service. For example, https://httpbin.org.
  • Path: The path to the specific resource or collection of resources. For example, /get is the path to the get endpoint.
  • HTTP Method: The HTTP method to be used when interacting with the endpoint. For example, GET is used to retrieve data, while POST is used to send data to the server.
  • Query Parameters: Additional parameters that can be passed to the endpoint. For example, ?page=1&limit=10 are query parameters that specify the page number and the number of items per page. They are separated from the path by a ? and from each other by a &. It is up to the API to define what query parameters are accepted.

HTTP Methods

HTTP methods define the type of operation to be performed on the resource. The most common HTTP methods are:

  • GET: Retrieve data from a server.
  • POST: Send data to a server to create a new resource.
  • PATCH: Send data to a server to update a resource.
  • DELETE: Remove a resource from the server.
  • PUT: Send data to a server to create or update a resource.

Warning

It is important to note that not all APIs follow the REST architecture strictly. Some APIs may use different HTTP methods or conventions. It is essential to refer to the API documentation to understand how to interact with a specific API. Especially PUT and PATCH are often used interchangeably, and in some cases the PUT implementation can vary widely between different APIs, and from the original specification. So the above is a guideline, not a strict rule.

A complete version of HTTP methods exists on Mozilla's MDN Web Docs.

Status Codes

HTTP status codes are returned by the server in response to a client's request. They provide information about the status of the request and the server's response.

Common HTTP Status Codes

  • 200 OK: The request was successful.
  • 201 Created: The request was successful, and a new resource was created.
  • 400 Bad Request: The request was malformed or invalid.
  • 401 Unauthorized: The request requires user authentication.
  • 403 Forbidden: The server understood the request but refused to authorize it.
  • 404 Not Found: The requested resource was not found. You're probably familiar with this one!
  • 429 Too Many Requests: The client has sent too many requests in a given amount of time.
  • 500 Internal Server Error: The server encountered an unexpected condition that prevented it from fulfilling the request.

A complete list of HTTP status codes can be found on the MDN Web Docs.

Perhaps you have noticed that the status codes are grouped into ranges. The first digit of the status code indicates the general category of the response:

  • 1xx: Informational responses.
  • 2xx: Success responses.
  • 3xx: Redirection responses.
  • 4xx: Client error responses.
  • 5xx: Server error responses.

Headers

HTTP headers provide additional information about the request or response. They are key-value pairs that are sent along with the request or response.

Common header parameters

  • Content-Type: Specifies the media type of the request or response body. For example, application/json indicates that the content is in JSON format.
  • Authorization: Contains credentials for authenticating the client with the server. For example, Bearer <token> is used for token-based authentication.
  • Accept: Specifies the media types that are acceptable for the response. For example, application/json indicates that the client prefers JSON responses. This is useful when the server can return different types of responses. (like XML, JSON, HTML, etc.)
  • Set-Cookie: Sets a cookie on the client's browser. This is used for session management and tracking.

A complete list of HTTP standard headers can be found on the MDN Web Docs.

Explanation of HTTP Cookies here: MDN Web Docs

Response Format and Body

The response from a REST API is typically in JSON format. JSON (JavaScript Object Notation) is a lightweight data interchange format that is easy for humans to read and write and easy for machines to parse and generate. It is based on key-value pairs and arrays, making it a popular choice for data exchange between systems. Other formats, such as XML, are also used in some APIs, including APIC and ISE.

For Python programmers, JSON data can be easily converted to Python dictionaries using the json module. This allows you to work with the data in a familiar way, using standard Python dictionary syntax. requests, niquests and httpx libraries provide methods to parse JSON data directly into Python dictionaries.

The first example requires you to connect to the jumpserver and run the code there. We already installed a mock API service for a Network Source of Truth on the jumpserver.

You can open the swagger documentation for the mock API, see the available endpoints and try them out.

Get a list of devices from NSOT
import requests

myid = "00"
base_path = f"http://nsot{myid}.p4ne.automation.wingmen.dk"

response = requests.get(base_path + "/devices")

Now, let's take a look at the response from the server.

Response and JSON Parsing
print(response.json())

print(type(response.json()))

print(response.status_code)

print(response.headers)
Output
[{'id': 1, 'hostname': 'switch_1'}]
<class 'list'>
200
{'Content-Length': '32', 'Content-Type': 'application/json', 'Date': 'Wed, 03 Dec 2025 07:10:44 GMT', 'Server': 'uvicorn'}

Using REST APIs in Python

Python provides several libraries to interact with REST APIs, including requests and niquests. Below are examples of how to use these libraries to perform various HTTP operations.

GET Request

A GET request retrieves data from a specified URL.

import requests

target_url = "https://httpbin.org/get"

# request the target URL, using requests library `get()` method
response = requests.get(target_url)
print(response.text)

POST Request

A POST request sends data to a server to create a new resource.

import requests

# --- POST ---
target_url = "https://httpbin.org/post"
payload = {
    "device_type": "router",
    "vendor": "Cisco",
    "model": "ISR4451",
}

# request the target URL, using requests library
response = requests.post(target_url, json=payload)

Let's take a look at the response from the server.

Response and JSON Parsing
print(response.json())

You might wonder what the .json() method does. It is a method that is available on the response object returned by the requests library. It parses the response content as JSON and returns a Python dictionary. This is useful when working with APIs that return JSON data, which is a common format for RESTful services.

Since we now have a dictionary, we can access the data using standard Python dictionary syntax. For example, to access the url key in the response, we can do the following:

Accessing Data in the Response
print(response.json()["url"])
Output
https://httpbin.org/post

Authentication and Token Retrieval

When working with REST APIs, you often need to authenticate your requests. One common method is using tokens. Let's walk through the process of retrieving and using a token for authentication step by step, using some intentional mistakes to highlight important concepts.

Step 1: Import Necessary Libraries

First, we need to import the necessary libraries. We'll use requests for making HTTP requests and getpass for securely entering the password.

Import Libraries
from getpass import getpass
import requests
import urllib3 # explained later

Step 2: Define the Base URL

Next, we define the base URL for the API. In this example, we'll use Cisco's Catalyst Center Always On Sandbox.

base_url = "https://sandboxdnac.cisco.com"

Step 3: Make an Initial Request

Let's start by making a request to an incorrect endpoint to see what happens.

First attempt at a GET request to CC Sandbox
devices_path = "/dna/intent/api/v1/network-devices"
response = requests.get(f"{base_url}{devices_path}")  # requests.exceptions.SSLError

Here, we encounter an SSL error because the server uses a self-signed certificate. To bypass this error, we can disable SSL verification.

Disable SSL Verification
response = requests.get(f"{base_url}{devices_path}", verify=False)  

Now, we get a 401 Unauthorized error because we haven't provided any authentication. And we still get warning messages about the SSL certificate. We can suppress these warnings by disabling them. That's the reason why we imported urllib3 at the beginning.

Disable SSL Warnings
urllib3.disable_warnings()

Now, let's solve the authentication issue. First we need to look in the documentation of the API to find out how to authenticate. It can be accessed here: API Documentation

We can see that we need to obtain a token by sending a POST request to the /dna/system/api/v1/auth/token endpoint path with our credentials.

Define the Authentication Endpoint
auth_path = "/dna/system/api/v1/auth/token"
auth_url = base_url + auth_path

For security reasons, it's best to prompt the user to enter their credentials at runtime rather than hardcoding them. Since CC expects the username and password, we'll use the auth parameter in the requests.post method to pass the credentials. This matches with the "Basic" authentication method requirement stated in the Catalyst Center documentation, where the username and password are encoded in the request header.

username = "devnetuser"
password = getpass()
auth = (username, password) # create a tuple with the username and password - this is the format requests expects

Let's send the request to the authentication endpoint and see what happens.

Send Authentication Request
response = requests.post(
    url=auth_url,
    auth=auth,
    verify=False
) 

if response.status_code == 200:
    token = response.json()["Token"]
    print("Token retrieved successfully")
    print(token)
else:
    print("Authentication error :(")
    exit(1)

Great, now we have the token! In order to use the token for subsequent requests, we need to set it in the headers of each request we send to the API. To avoid having to do this manually each time, we can create a session and set the token in the headers. The session can be reused for multiple requests, and the token will be automatically included in the headers of each request.

Create a Session and Set the Token
session = requests.Session()
session.headers.update({"x-auth-token": token}) # set the token in the headers as specified in the API documentation (top of page)

Let's try to retrieve a list of network devices using the incorrect endpoint again to see what happens.

response = session.get(f"{base_url}{devices_path}", verify=False)  # 404 Not Found
if response.status_code == 200:
    print(response.json())
else:
    print("Error fetching devices")  # we don't actually know what the error is
    exit(1)

Here, we get a 404 Not Found error because the endpoint is incorrect. Instead of just printing a generic error message, we should raise an exception to get more details.

Raise an Exception for Errors
response.raise_for_status()  # do this instead!

Now, let's correct the endpoint and try again.

devices_path = "/dna/intent/api/v1/network-device"  # correct path!
response = session.get(f"{base_url}{devices_path}", verify=False)  # 200 OK

# Check for errors
response.raise_for_status() # no error

# Print device information
for device in response.json()["response"]:
    print(f"Hostname: {device['hostname']}")
    print(f"Management IP: {device['managementIpAddress']}")
    print(f"Platform ID: {device['platformId']}")
    print(f"Software Type: {device['softwareType']}")
    print(f"Software Version: {device['softwareVersion']}")
    print("-" * 20)

Conclusion

By intentionally making mistakes and correcting them, we've highlighted important aspects of working with REST APIs, such as handling SSL errors, authentication, and error handling. This approach not only helps in understanding the concepts but also prepares you to troubleshoot common issues you might encounter.

Feel free to ask if you need further assistance or have any specific questions!