Robot Framework API Testing with RequestsLibrary
Robot Framework isn't just for UI testing. With RequestsLibrary, it becomes a capable API testing tool — one that produces the same structured reports and follows the same keyword model as your UI tests. This matters for teams that want unified test output and want non-developers to be able to write API tests.
This guide covers everything from basic HTTP requests to production-grade API test patterns.
Installation
pip install robotframework
pip install robotframework-requestsNote the package name: robotframework-requests, but you import it as RequestsLibrary.
*** Settings ***
Library RequestsLibrary
Library Collections
Library String
Library JSONLibrary # Optional, for complex JSON operationsAlso install JSONLibrary if you need advanced JSON querying:
pip install robotframework-jsonlibraryBasic HTTP Requests
RequestsLibrary uses a session model — you create a session first, then make requests against it. This handles base URLs, headers, and authentication at the session level.
*** Variables ***
${BASE_URL} https://api.example.com
${API_KEY} %{API_KEY} # Read from environment variable
*** Test Cases ***
GET Request Returns Expected Response
Create Session api ${BASE_URL}
${response}= GET On Session api /users/1
Should Be Equal As Integers ${response.status_code} 200
${body}= Set Variable ${response.json()}
Should Be Equal ${body}[name] John Doe
Should Be Equal ${body}[email] john@example.com
POST Request Creates Resource
Create Session api ${BASE_URL}
${headers}= Create Dictionary Content-Type=application/json
${body}= Create Dictionary
... name=Jane Smith
... email=jane@example.com
... role=viewer
${response}= POST On Session api /users json=${body} headers=${headers}
Should Be Equal As Integers ${response.status_code} 201
Should Be Equal ${response.json()}[email] jane@example.com
[Teardown] DELETE On Session api /users/${response.json()}[id]
PUT Request Updates Resource
Create Session api ${BASE_URL}
${body}= Create Dictionary name=Updated Name
${response}= PUT On Session api /users/1 json=${body}
Should Be Equal As Integers ${response.status_code} 200
DELETE Request Removes Resource
Create Session api ${BASE_URL}
${response}= DELETE On Session api /users/99
Should Be Equal As Integers ${response.status_code} 204Session configuration at creation:
Create Session
... alias=api
... url=${BASE_URL}
... headers={"Authorization": "Bearer ${TOKEN}", "Accept": "application/json"}
... verify=True # SSL verification (set False for self-signed certs)
... timeout=30Authentication Patterns
Bearer token auth:
*** Keywords ***
Create Authenticated Session
[Arguments] ${token}
${headers}= Create Dictionary Authorization=Bearer ${token}
Create Session api ${BASE_URL} headers=${headers}
Get Auth Token
Create Session auth ${BASE_URL}
${body}= Create Dictionary
... username=${API_USERNAME}
... password=${API_PASSWORD}
${response}= POST On Session auth /auth/login json=${body}
Should Be Equal As Integers ${response.status_code} 200
${token}= Set Variable ${response.json()}[access_token]
RETURN ${token}
*** Test Cases ***
Authenticated Endpoint Returns Data
${token}= Get Auth Token
Create Authenticated Session ${token}
${response}= GET On Session api /me
Should Be Equal As Integers ${response.status_code} 200
Should Be Equal ${response.json()}[email] ${API_USERNAME}Basic auth:
Create Session api ${BASE_URL} auth=${('username', 'password')}API key in header:
${headers}= Create Dictionary X-API-Key=${API_KEY}
Create Session api ${BASE_URL} headers=${headers}OAuth2 client credentials:
*** Keywords ***
Get OAuth2 Token
Create Session oauth ${AUTH_SERVER_URL}
${body}= Create Dictionary
... grant_type=client_credentials
... client_id=${CLIENT_ID}
... client_secret=${CLIENT_SECRET}
... scope=read write
${response}= POST On Session oauth /oauth/token data=${body}
Should Be Equal As Integers ${response.status_code} 200
RETURN ${response.json()}[access_token]Response Validation
Status code validation:
# Basic
Should Be Equal As Integers ${response.status_code} 200
# With custom message
Run Keyword If ${response.status_code} != 200
... Fail Expected 200, got ${response.status_code}: ${response.text}JSON body validation:
*** Keywords ***
Validate User Response Body
[Arguments] ${response}
${body}= Set Variable ${response.json()}
# Required fields present
Dictionary Should Contain Key ${body} id
Dictionary Should Contain Key ${body} email
Dictionary Should Contain Key ${body} created_at
# Field types and values
Should Be True isinstance(${body}[id], int)
Should Match Regexp ${body}[email] ^[^@]+@[^@]+\.[^@]+$
Should Be True ${body}[id] > 0
Validate Paginated Response
[Arguments] ${response} ${expected_per_page}=20
${body}= Set Variable ${response.json()}
Dictionary Should Contain Key ${body} data
Dictionary Should Contain Key ${body} total
Dictionary Should Contain Key ${body} page
${count}= Get Length ${body}[data]
Should Be True ${count} <= ${expected_per_page}Header validation:
${content_type}= Get From Dictionary ${response.headers} Content-Type
Should Contain ${content_type} application/jsonResponse time:
${elapsed}= Set Variable ${response.elapsed.total_seconds()}
Should Be True ${elapsed} < 2.0 msg=API took ${elapsed}s, expected under 2sJSON Handling
Working with nested JSON:
*** Test Cases ***
Nested JSON Access
Create Session api ${BASE_URL}
${response}= GET On Session api /users/1
${body}= Set Variable ${response.json()}
# Direct key access
${name}= Set Variable ${body}[name]
# Nested access
${city}= Set Variable ${body}[address][city]
# Array access
${first_tag}= Set Variable ${body}[tags][0]
# Length check
${tag_count}= Get Length ${body}[tags]
Should Be True ${tag_count} >= 1Building request bodies:
*** Keywords ***
Build Create Project Request
[Arguments] ${name} ${description} @{member_emails}
${members}= Create List
FOR ${email} IN @{member_emails}
${member}= Create Dictionary email=${email} role=viewer
Append To List ${members} ${member}
END
${body}= Create Dictionary
... name=${name}
... description=${description}
... members=${members}
... settings={"notifications": true, "public": false}
RETURN ${body}
*** Test Cases ***
Create Project With Members
${body}= Build Create Project Request
... My Test Project
... A project for testing
... alice@example.com
... bob@example.com
Create Session api ${BASE_URL} headers=${AUTH_HEADERS}
${response}= POST On Session api /projects json=${body}
Should Be Equal As Integers ${response.status_code} 201
${member_count}= Get Length ${response.json()}[members]
Should Be Equal As Integers ${member_count} 2Parsing complex responses with JSONLibrary:
Library JSONLibrary
*** Keywords ***
Extract All User Emails From Response
[Arguments] ${response}
${emails}= Get Value From Json ${response.json()} $..email
RETURN ${emails}
Find Items With High Priority
[Arguments] ${response}
${items}= Get Value From Json ${response.json()} $.items[?(@.priority=='high')]
RETURN ${items}JSONLibrary uses JSONPath syntax — the $..email expression finds all email fields anywhere in the document.
Chaining Requests
Most real API tests need to chain requests — create a resource, then operate on it:
*** Test Cases ***
Full User Lifecycle
[Setup] Create Authenticated Session ${AUTH_TOKEN}
# Create user
${create_body}= Create Dictionary
... email=test-${RANDOM_STRING}@example.com
... name=Test User
${create_resp}= POST On Session api /users json=${create_body}
Should Be Equal As Integers ${create_resp.status_code} 201
${user_id}= Set Variable ${create_resp.json()}[id]
# Verify user exists
${get_resp}= GET On Session api /users/${user_id}
Should Be Equal As Integers ${get_resp.status_code} 200
Should Be Equal ${get_resp.json()}[email] ${create_body}[email]
# Update user
${update_body}= Create Dictionary name=Updated Name
${update_resp}= PUT On Session api /users/${user_id} json=${update_body}
Should Be Equal As Integers ${update_resp.status_code} 200
Should Be Equal ${update_resp.json()}[name] Updated Name
# Delete user
${delete_resp}= DELETE On Session api /users/${user_id}
Should Be Equal As Integers ${delete_resp.status_code} 204
# Verify deleted
${gone_resp}= GET On Session api /users/${user_id} expected_status=404
Should Be Equal As Integers ${gone_resp.status_code} 404
[Teardown] Delete Sessions
*** Keywords ***
Create Authenticated Session
[Arguments] ${token}
${headers}= Create Dictionary
... Authorization=Bearer ${token}
... Content-Type=application/json
... Accept=application/json
Create Session api ${BASE_URL} headers=${headers}The expected_status=404 argument tells RequestsLibrary not to raise an exception for 4xx responses — useful when you're deliberately testing error cases.
Handling Async APIs
For APIs that return 202 Accepted and process asynchronously:
*** Keywords ***
Submit Job And Wait For Completion
[Arguments] ${job_payload} ${max_wait}=60s
# Submit the job
${submit_resp}= POST On Session api /jobs json=${job_payload}
Should Be Equal As Integers ${submit_resp.status_code} 202
${job_id}= Set Variable ${submit_resp.json()}[job_id]
# Poll until complete
Wait Until Keyword Succeeds ${max_wait} 3s
... Job Should Be Complete ${job_id}
# Get final result
${result_resp}= GET On Session api /jobs/${job_id}/result
Should Be Equal As Integers ${result_resp.status_code} 200
RETURN ${result_resp}
Job Should Be Complete
[Arguments] ${job_id}
${resp}= GET On Session api /jobs/${job_id}
Should Be Equal As Integers ${resp.status_code} 200
${status}= Set Variable ${resp.json()}[status]
Should Be Equal ${status} completed
... msg=Job ${job_id} status is ${status}, expected completedQuery Parameters and URL Building
*** Keywords ***
Search Users By Email Domain
[Arguments] ${domain} ${page}=1 ${per_page}=20
${params}= Create Dictionary
... email_domain=${domain}
... page=${page}
... per_page=${per_page}
${response}= GET On Session api /users params=${params}
RETURN ${response}
*** Test Cases ***
Search Returns Filtered Results
Create Session api ${BASE_URL} headers=${AUTH_HEADERS}
${resp}= Search Users By Email Domain example.com
Should Be Equal As Integers ${resp.status_code} 200
${users}= Set Variable ${resp.json()}[data]
FOR ${user} IN @{users}
Should Contain ${user}[email] @example.com
ENDError Response Testing
Testing error cases is as important as the happy path:
*** Test Cases ***
Missing Required Field Returns 422
Create Session api ${BASE_URL} headers=${AUTH_HEADERS}
${body}= Create Dictionary email=test@example.com # Missing required 'name'
${response}= POST On Session api /users json=${body}
... expected_status=422
Should Be Equal As Integers ${response.status_code} 422
${errors}= Set Variable ${response.json()}[errors]
Should Be True 'name' in ${errors}
Unauthorized Access Returns 401
Create Session unauth ${BASE_URL} # No auth headers
${response}= GET On Session unauth /protected-endpoint
... expected_status=401
Should Be Equal As Integers ${response.status_code} 401
Rate Limiting Returns 429
Create Session api ${BASE_URL} headers=${AUTH_HEADERS}
# Make requests until rate limited
FOR ${i} IN RANGE 100
${response}= GET On Session api /rate-limited-endpoint
... expected_status=any
Exit For Loop If ${response.status_code} == 429
END
Should Be Equal As Integers ${response.status_code} 429
Dictionary Should Contain Key ${response.headers} Retry-AfterTest Data Setup with API Calls
Use the API itself for test data setup instead of database manipulation when possible:
*** Keywords ***
Create Test User Via API
[Documentation] Creates a test user and returns the user data dict
[Arguments] ${role}=viewer
Create Session admin_api ${BASE_URL} headers=${ADMIN_HEADERS}
${email}= Generate Random String 8 [LETTERS]
${email}= Set Variable ${email.lower()}@test-${SUITE NAME}.example.com
${body}= Create Dictionary
... email=${email}
... name=Test User
... role=${role}
... password=TestPass123!
${response}= POST On Session admin_api /admin/users json=${body}
Should Be Equal As Integers ${response.status_code} 201
RETURN ${response.json()}
Delete Test User Via API
[Arguments] ${user_id}
Create Session admin_api ${BASE_URL} headers=${ADMIN_HEADERS}
DELETE On Session admin_api /admin/users/${user_id}
*** Test Cases ***
Viewer Cannot Access Admin Endpoint
${user}= Create Test User Via API viewer
${token}= Get Token For User ${user}[email] TestPass123!
${headers}= Create Dictionary Authorization=Bearer ${token}
Create Session viewer_session ${BASE_URL} headers=${headers}
${response}= GET On Session viewer_session /admin/users
... expected_status=403
Should Be Equal As Integers ${response.status_code} 403
[Teardown] Delete Test User Via API ${user}[id]Suite Structure for API Tests
tests/api/
suite_setup.robot # Auth setup, shared variables
users/
users_crud.robot
users_search.robot
users_permissions.robot
projects/
projects_crud.robot
projects_members.robot
errors/
validation_errors.robot
auth_errors.robot
resources/
api/
auth.resource
users.resource
projects.resourceShared suite-level auth setup:
# tests/api/suite_setup.robot — included by all API test suites
*** Settings ***
Resource ../../resources/api/auth.resource
*** Variables ***
${BASE_URL} %{API_BASE_URL}
${ADMIN_TOKEN} ${EMPTY}
${USER_TOKEN} ${EMPTY}
*** Keywords ***
Initialize API Test Session
${admin_token}= Get Admin Auth Token
${user_token}= Get User Auth Token viewer@example.com ViewerPass123!
Set Suite Variable ${ADMIN_TOKEN} ${admin_token}
Set Suite Variable ${USER_TOKEN} ${user_token}
${admin_headers}= Create Dictionary Authorization=Bearer ${ADMIN_TOKEN}
${user_headers}= Create Dictionary Authorization=Bearer ${USER_TOKEN}
Set Suite Variable ${ADMIN_HEADERS} ${admin_headers}
Set Suite Variable ${USER_HEADERS} ${user_headers}
Create Session admin_api ${BASE_URL} headers=${ADMIN_HEADERS}
Create Session user_api ${BASE_URL} headers=${USER_HEADERS}When Robot Framework API Testing Makes Sense
The main advantage over raw Python API test frameworks (pytest + requests, or Postman) is report unification. If your team already uses RF for UI tests, adding API tests in RF means all results flow into the same HTML reports, the same CI pipeline, and use the same keyword vocabulary.
The disadvantage: complex data manipulation (sorting, filtering, mathematical assertions) is more verbose in RF than in Python. A complex JSON schema validation that's 5 lines in pytest becomes 20 lines in RF.
For teams with mixed technical backgrounds or teams already invested in RF, RequestsLibrary is a solid choice. For developer-only teams doing pure API testing, pytest + requests is simpler. The tradeoff is the same as for RF in general: readability and accessibility vs raw expressiveness.