Nova Act QA Studio provides a RESTful API for managing test automation workflows, including test execution, OAuth client management, and artifact handling. All endpoints require authentication via AWS Cognito and use OAuth 2.0 scopes for authorization.
Base URL: https://{api-gateway-id}.execute-api.{region}.amazonaws.com/{stage}/
API Version: 1.2.0
- Authentication
- Usecase Management Endpoints
- Execution Endpoints
- Device Farm Endpoints
- Test Suite Endpoints
- OAuth Client Management
- Error Handling
- Rate Limiting
- Pagination
All API requests must include a valid JWT token in the Authorization header:
Authorization: Bearer <jwt_token>For CI/CD integration and machine-to-machine (M2M) authentication, use the OAuth 2.0 client credentials flow:
Step 1: Create OAuth Client
Create an OAuth client via the web UI or API (requires user authentication):
curl -X POST 'https://api.example.com/api/oauth-clients' \
-H 'Authorization: Bearer <user_jwt_token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "CI/CD Runner - Production",
"scopes": ["api/suite.read", "api/suite.write", "api/execution.write"]
}'Response:
{
"client_id": "7abc123def456",
"client_secret": "secret_xyz789...",
"created_date": "2026-02-16T12:00:00Z"
}IMPORTANT: Save the client_secret - it will only be shown once!
Step 2: Obtain Access Token
Use the client credentials to obtain an access token from Cognito:
curl -X POST \
'https://{cognito-domain}.auth.{region}.amazoncognito.com/oauth2/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials' \
-d 'client_id=7abc123def456' \
-d 'client_secret=secret_xyz789...' \
-d 'scope=api/suite.read api/suite.write api/execution.write'Response:
{
"access_token": "eyJraWQiOiJ...",
"expires_in": 3600,
"token_type": "Bearer"
}Step 3: Use Access Token
Include the access token in all API requests:
curl -X POST 'https://api.example.com/usecase/usecase-123/execute' \
-H 'Authorization: Bearer eyJraWQiOiJ...' \
-H 'Content-Type: application/json'- Token Lifetime: Access tokens typically expire after 1 hour (3600 seconds)
- Token Refresh: No refresh tokens are issued for client credentials flow - request a new token when expired
- Token Storage: Store credentials securely using environment variables or secrets manager
- Token Validation: Tokens are validated on every API request
Scopes follow the pattern api/{resource}.{permission}:
| Scope | Description | Access Level |
|---|---|---|
api/admin |
Full access to all resources | Admin |
api/execution.write |
Create and update executions | Write |
api/execution.read |
Read execution data | Read |
api/usecases.write |
Create and update usecases | Write |
api/usecases.read |
Read usecase data | Read |
api/suite.write |
Manage test suites | Write |
api/suite.read |
Read test suite data | Read |
api/oauth-clients.write |
Create, delete, and rotate OAuth clients | Write |
api/oauth-clients.read |
List OAuth clients | Read |
Scope Requirements by Operation:
| Operation | Required Scopes |
|---|---|
| Execute test suite | api/suite.write, api/execution.write |
| Read test suite | api/suite.read |
| Create execution | api/execution.write |
| Update step status | api/execution.write |
| Upload artifacts | api/execution.write |
| Read execution data | api/execution.read |
| Manage OAuth clients | api/oauth-clients.write, api/oauth-clients.read |
Best Practice: Grant only the minimum scopes required for your use case.
Usecase management endpoints allow you to create, read, and update test usecases, including enabling step caching for performance optimization.
Create a new test usecase with optional step caching enabled.
Endpoint: POST /usecases
Required Scopes: api/usecases.write or api/admin
Request Body:
{
"name": "Login with valid credentials",
"description": "Test user login flow with valid username and password",
"steps": [
{
"type": "navigation",
"url": "https://example.com/login",
"description": "Navigate to login page"
},
{
"type": "action",
"action": "fill",
"selector": "#username",
"value": "testuser",
"description": "Enter username"
}
],
"enableCache": true
}Request Body Fields:
name(string, required) - Usecase namedescription(string, optional) - Usecase descriptionsteps(array, required) - List of test stepsenableCache(boolean, optional, default: false) - Enable step caching for navigation stepstest_platform(string, optional, default: "web") - Test platform:"web"or"mobile"platform(string, optional) - Mobile platform:"ANDROID"or"IOS"(required when test_platform is "mobile")app_package(string, optional) - Android app package name (e.g.,"com.example.myapp")app_activity(string, optional) - Android app activity (e.g.,"com.example.myapp.MainActivity")bundle_id(string, optional) - iOS bundle identifier (e.g.,"com.example.myapp")device_arn(string, optional) - Device Farm device ARN (auto-selects newest device if omitted)
Response (201 Created):
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Login with valid credentials",
"description": "Test user login flow with valid username and password",
"enableCache": true,
"created_at": "2026-03-03T10:00:00Z",
"updated_at": "2026-03-03T10:00:00Z"
}Error Responses:
400 Bad Request- Missing required fields or invalid data401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes500 Internal Server Error- Server error
Example (cURL):
curl -X POST \
'https://api.example.com/usecases' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "Login with valid credentials",
"description": "Test user login flow",
"steps": [...],
"enableCache": true
}'Example (Python):
import requests
response = requests.post(
'https://api.example.com/usecases',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={
'name': 'Login with valid credentials',
'description': 'Test user login flow',
'steps': [...],
'enableCache': True
}
)
data = response.json()
usecase_id = data['id']
print(f"Usecase created: {usecase_id} (cache enabled: {data['enableCache']})")Retrieve a usecase by ID, including cache status and metadata.
Endpoint: GET /usecases/{id}
Path Parameters:
id(string, required) - Usecase ID
Required Scopes: api/usecases.read or api/admin
Response (200 OK):
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Login with valid credentials",
"description": "Test user login flow with valid username and password",
"enableCache": true,
"created_at": "2026-03-03T10:00:00Z",
"updated_at": "2026-03-03T10:15:00Z",
"steps": [
{
"id": "step-001",
"type": "navigation",
"url": "https://example.com/login",
"description": "Navigate to login page"
}
]
}Response Fields:
id- Usecase unique identifiername- Usecase namedescription- Usecase descriptionenableCache- Whether step caching is enabledcreated_at- ISO8601 timestamp of creationupdated_at- ISO8601 timestamp of last updatesteps- List of test steps
Error Responses:
401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes404 Not Found- Usecase not found500 Internal Server Error- Server error
Example (cURL):
curl -X GET \
'https://api.example.com/usecases/01234567-89ab-cdef-0123-456789abcdef' \
-H 'Authorization: Bearer <token>'Example (Python):
import requests
response = requests.get(
f'https://api.example.com/usecases/{usecase_id}',
headers={'Authorization': f'Bearer {access_token}'}
)
data = response.json()
print(f"Usecase: {data['name']}")
print(f"Cache enabled: {data['enableCache']}")Update an existing usecase, including enabling or disabling step caching.
Endpoint: PATCH /usecases/{id}
Path Parameters:
id(string, required) - Usecase ID
Required Scopes: api/usecases.write or api/admin
Request Body:
{
"name": "Login with valid credentials (updated)",
"description": "Updated description",
"enableCache": true
}Request Body Fields (all optional):
name(string) - Updated usecase namedescription(string) - Updated descriptionsteps(array) - Updated test stepsenableCache(boolean) - Enable or disable step caching
Response (200 OK):
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Login with valid credentials (updated)",
"description": "Updated description",
"enableCache": true,
"updated_at": "2026-03-03T10:30:00Z"
}Error Responses:
400 Bad Request- Invalid data401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes404 Not Found- Usecase not found500 Internal Server Error- Server error
Example (cURL):
curl -X PATCH \
'https://api.example.com/usecases/01234567-89ab-cdef-0123-456789abcdef' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"enableCache": true
}'Example (Python):
import requests
response = requests.patch(
f'https://api.example.com/usecases/{usecase_id}',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={'enableCache': True}
)
data = response.json()
print(f"Cache enabled: {data['enableCache']}")Retrieve all steps for a usecase, including cache metadata for cached steps.
Endpoint: GET /usecases/{id}/steps
Path Parameters:
id(string, required) - Usecase ID
Required Scopes: api/usecases.read or api/admin
Response (200 OK):
{
"usecase_id": "01234567-89ab-cdef-0123-456789abcdef",
"steps": [
{
"id": "step-001",
"sort": 1,
"type": "navigation",
"url": "https://example.com/login",
"description": "Navigate to login page",
"cacheable": true
},
{
"id": "step-002",
"sort": 2,
"type": "action",
"action": "fill",
"selector": "#username",
"value": "testuser",
"description": "Enter username",
"cacheable": false
}
],
"cachedSteps": "[{\"action\":\"goto\",\"url\":\"https://example.com/login\",\"options\":{\"waitUntil\":\"networkidle\"}}]",
"cacheLastUpdated": "2026-03-03T10:15:00Z"
}Response Fields:
usecase_id- Usecase unique identifiersteps- List of test steps with metadatacacheable- Whether this step type can be cached (navigation steps only)
cachedSteps- JSON string of cached Playwright actions (null if no cache)cacheLastUpdated- ISO8601 timestamp when cache was last built (null if no cache)
Error Responses:
401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes404 Not Found- Usecase not found500 Internal Server Error- Server error
Example (cURL):
curl -X GET \
'https://api.example.com/usecases/01234567-89ab-cdef-0123-456789abcdef/steps' \
-H 'Authorization: Bearer <token>'Example (Python):
import requests
import json
response = requests.get(
f'https://api.example.com/usecases/{usecase_id}/steps',
headers={'Authorization': f'Bearer {access_token}'}
)
data = response.json()
print(f"Total steps: {len(data['steps'])}")
if data['cachedSteps']:
cached_actions = json.loads(data['cachedSteps'])
print(f"Cached actions: {len(cached_actions)}")
print(f"Cache last updated: {data['cacheLastUpdated']}")
else:
print("No cache available")The following table summarizes all cache-related fields across usecase endpoints:
| Field | Type | Default | Endpoints | Description |
|---|---|---|---|---|
enableCache |
boolean | false | POST /usecases, PATCH /usecases, GET /usecases | Whether step caching is enabled for this usecase |
cachedSteps |
string (JSON) | null | GET /usecases/{id}/steps | JSON-serialized list of cached Playwright actions |
cacheLastUpdated |
string (ISO8601) | null | GET /usecases/{id}/steps | Timestamp when cache was last built |
cacheable |
boolean | - | GET /usecases/{id}/steps | Whether a specific step type can be cached (read-only, computed field) |
Cache Building Process:
- Usecase executes successfully with
enableCache: true - EventBridge triggers cache builder Lambda asynchronously
- Lambda parses Nova Act responses from S3
- Lambda stores cached Playwright actions in DynamoDB
- Subsequent executions use cached actions for navigation steps
Cache Execution Behavior:
- First execution: Cache miss (normal execution time)
- Subsequent executions: Cache hit (40-60% faster for navigation steps)
- Cache failures: Automatic fallback to Nova Act
Notes:
- Only navigation steps are cacheable (goto, goBack, goForward, reload)
- Cache builds asynchronously after successful execution
- Cache is invalidated when usecase steps are modified
- Cache execution failures automatically fall back to Nova Act
Fields specific to steps of type network_assertion. All fields except network_url_pattern are optional. Defaults match the worker-side fallback behaviour.
Request-side fields:
| Field | Type | Default | Description |
|---|---|---|---|
network_url_pattern |
string | — (required) | Playwright glob pattern identifying the request, e.g. **/api/users |
network_method |
string | empty | Expected HTTP verb: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Empty means no method check. |
network_request_body |
string (JSON) | null | Expected request body, interpreted per network_body_match_type |
network_body_match_type |
string | exact |
One of exact, subset, schema. See "Match Types" below. |
Mock response fields:
| Field | Type | Default | Description |
|---|---|---|---|
network_mock_response |
string (JSON) | null | {"status": …, "body": …, "headers": …}. When set, the matching request is intercepted. |
network_mock_passthrough |
boolean | false |
If true, fetch the real response and merge network_mock_response fields over it. If false, the real request is never sent. |
Response-side assertion fields:
| Field | Type | Default | Description |
|---|---|---|---|
network_response_status |
integer | null | Exact-match expected HTTP status code. Must be in [100, 599] when set. |
network_response_body |
string (JSON) | null | Expected response body, interpreted per network_response_body_match_type |
network_response_body_match_type |
string | subset |
One of subset or schema. exact is not permitted on the response side because response payloads frequently contain non-deterministic values. |
Timing:
| Field | Type | Default | Description |
|---|---|---|---|
network_timeout |
integer | 15 |
Seconds to wait for a matching request, capped at 120. |
Match Types:
exact— parsed-JSON equality between expected and captured body. Request side only.subset— every key/value in the expected template must be present in the captured body with matching values. Extra keys are ignored. Arrays match element-by-element; lengths must match.schema— the expected body is a JSON Schema Draft 2020-12 document. The captured body is validated against the schema. External$ref(http/https/file) is rejected; only local-pointer refs (#/...) are allowed.
Size limits:
Request body, response body, mock response, and schema documents are each capped in bytes. The default is 1 MiB (1_048_576) on both server and client. Operators can override the server-side cap via the networkAssertionBodyMaxBytes setting in configuration.json, which flows to the worker and Lambda environment as NETWORK_ASSERTION_BODY_MAX_BYTES. The CLI uses a fixed 1 MiB default for pre-submit validation and relies on the server for authoritative enforcement.
The captured response body is always size-checked, even when no response body assertion is configured, so oversized responses surface early.
Security:
- External
$reftargets (http://,https://,file://) in schema documents are rejected at validation time to prevent SSRF and file-read attacks. - The subset matcher refuses JSON nested deeper than 20 levels.
- Captured bodies are truncated to 500 characters in logs. Only a compact match summary (e.g.
method=POST body_match=subset:pass resp_status=201 resp_body=schema:pass) is persisted in execution records. - Route handlers registered via
network_mock_responseare always torn down on step exit, even on Nova Act failure, timeout, or assertion failure.
Caching:
network_assertion steps are intentionally not cached. The Nova Act action portion participates in the usecase's existing caching ladder (trajectory replay / Playwright cache) identically to a navigation step, but the interception and assertion logic always runs fresh. An API contract change must never be masked by a cache hit.
Create and start a usecase execution.
Endpoint: POST /usecase/{id}/execute
Path Parameters:
id(string, required) - Usecase ID
Query Parameters:
trigger-type(string, optional) - Execution trigger typeOnDemand(default) - Queue execution for worker processingScheduled- Direct ECS task execution (used by EventBridge)OnDemandHeadless- Direct ECS task execution (used by UI)ci_runner- Create execution record only, no ECS task (for CI/CD)
suite-execution-id(string, optional) - Suite execution ID if part of a suitesuite-id(string, optional) - Suite ID if part of a suite
Required Scopes: api/execution.write or api/admin
Request Body: None
Response (OnDemand):
{
"status": "usecase queued",
"usecaseId": "usecase-123"
}Response (Scheduled/OnDemandHeadless):
{
"status": "task started",
"usecaseId": "usecase-123",
"executionId": "execution-456",
"taskArn": "arn:aws:ecs:us-east-1:123456789:task/cluster/task-id",
"taskId": "task-id",
"cloudWatchLogsUrl": "https://console.aws.amazon.com/cloudwatch/..."
}Response (ci_runner):
{
"status": "execution created",
"usecaseId": "usecase-123",
"executionId": "execution-456"
}Error Responses:
400 Bad Request- Missing usecase ID or invalid trigger type401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes404 Not Found- Usecase not found500 Internal Server Error- Server error
Example (cURL):
curl -X POST \
'https://api.example.com/usecase/usecase-123/execute?trigger-type=ci_runner' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json'Example (Python):
import requests
response = requests.post(
'https://api.example.com/usecase/usecase-123/execute',
params={'trigger-type': 'ci_runner'},
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
)
data = response.json()
execution_id = data['executionId']
print(f"Execution created: {execution_id}")Example (JavaScript):
const response = await fetch(
'https://api.example.com/usecase/usecase-123/execute?trigger-type=ci_runner',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
console.log(`Execution created: ${data.executionId}`);Update the status of an individual execution step. Used by CI/CD runners to report step progress.
Endpoint: PATCH /usecase/{id}/executions/{executionId}/steps/{stepId}/status
Path Parameters:
id(string, required) - Usecase IDexecutionId(string, required) - Execution IDstepId(string, required) - Step ID
Required Scopes: api/execution.write or api/admin
Request Body:
{
"status": "running",
"started_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:35:00Z",
"error_message": "Element not found"
}Request Body Fields:
status(string, required) - Step status- Valid values:
pending,running,completed,failed,skipped
- Valid values:
started_at(string, optional) - ISO8601 timestamp when step startedcompleted_at(string, optional) - ISO8601 timestamp when step completederror_message(string, optional) - Error message for failed steps
Response:
{
"success": true,
"step_id": "step-789",
"status": "running"
}Error Responses:
400 Bad Request- Invalid status value or missing required fields401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes404 Not Found- Execution or step not found500 Internal Server Error- Server error
Example (cURL):
curl -X PATCH \
'https://api.example.com/usecase/usecase-123/executions/execution-456/steps/step-789/status' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"status": "completed",
"started_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:35:00Z"
}'Example (Python):
import requests
from datetime import datetime
response = requests.patch(
f'https://api.example.com/usecase/{usecase_id}/executions/{execution_id}/steps/{step_id}/status',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={
'status': 'completed',
'started_at': datetime.utcnow().isoformat() + 'Z',
'completed_at': datetime.utcnow().isoformat() + 'Z'
}
)
print(f"Step status updated: {response.json()}")Example (JavaScript):
const response = await fetch(
`https://api.example.com/usecase/${usecaseId}/executions/${executionId}/steps/${stepId}/status`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'completed',
started_at: new Date().toISOString(),
completed_at: new Date().toISOString()
})
}
);
const data = await response.json();
console.log('Step status updated:', data);Generate a presigned S3 URL for uploading execution-level artifacts (recording, logs). This allows CI/CD runners to upload large files directly to S3, bypassing API Gateway payload limits.
Endpoint: POST /usecase/{id}/executions/{executionId}/artifacts
Path Parameters:
id(string, required) - Usecase IDexecutionId(string, required) - Execution ID
Required Scopes: api/execution.write or api/admin
Request Body:
{
"type": "recording",
"filename": "recording.webm",
"content_type": "video/webm"
}Request Body Fields:
type(string, required) - Artifact type- Valid values:
recording,logs
- Valid values:
filename(string, required) - Original filenamecontent_type(string, required) - MIME type- For recording:
video/webm,video/mp4 - For logs:
text/plain
- For recording:
Response:
{
"artifact_id": "01933d7e-8f2a-7890-abcd-ef1234567890",
"upload_url": "https://s3.amazonaws.com/bucket/artifacts/usecase-123/executions/execution-456/recording.webm?X-Amz-Algorithm=...",
"expires_in": 3600,
"s3_key": "artifacts/usecase-123/executions/execution-456/recording.webm"
}Response Fields:
artifact_id- Unique artifact identifierupload_url- Presigned S3 URL for uploading (expires in 1 hour)expires_in- URL expiration time in secondss3_key- S3 object key where file will be stored
Error Responses:
400 Bad Request- Invalid artifact type, missing fields, or invalid content type401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes404 Not Found- Execution not found500 Internal Server Error- Server error
Example (cURL):
# Step 1: Get presigned URL
RESPONSE=$(curl -X POST \
'https://api.example.com/usecase/usecase-123/executions/execution-456/artifacts' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"type": "recording",
"filename": "recording.webm",
"content_type": "video/webm"
}')
UPLOAD_URL=$(echo $RESPONSE | jq -r '.upload_url')
# Step 2: Upload file to S3 using presigned URL
curl -X PUT "$UPLOAD_URL" \
-H 'Content-Type: video/webm' \
--data-binary @recording.webmExample (Python):
import requests
# Step 1: Get presigned URL
response = requests.post(
f'https://api.example.com/usecase/{usecase_id}/executions/{execution_id}/artifacts',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={
'type': 'recording',
'filename': 'recording.webm',
'content_type': 'video/webm'
}
)
data = response.json()
upload_url = data['upload_url']
# Step 2: Upload file to S3
with open('recording.webm', 'rb') as f:
upload_response = requests.put(
upload_url,
headers={'Content-Type': 'video/webm'},
data=f
)
print(f"Upload status: {upload_response.status_code}")Example (JavaScript):
// Step 1: Get presigned URL
const response = await fetch(
`https://api.example.com/usecase/${usecaseId}/executions/${executionId}/artifacts`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'recording',
filename: 'recording.webm',
content_type: 'video/webm'
})
}
);
const data = await response.json();
// Step 2: Upload file to S3
const uploadResponse = await fetch(data.upload_url, {
method: 'PUT',
headers: {
'Content-Type': 'video/webm'
},
body: recordingBlob
});
console.log(`Upload status: ${uploadResponse.status}`);Generate a presigned S3 URL for uploading step-level artifacts (screenshots, traces). This allows CI/CD runners to upload step-specific files directly to S3.
Endpoint: POST /usecase/{id}/executions/{executionId}/steps/{stepId}/artifacts
Path Parameters:
id(string, required) - Usecase IDexecutionId(string, required) - Execution IDstepId(string, required) - Step ID
Required Scopes: api/execution.write or api/admin
Request Body:
{
"filename": "screenshot.png",
"content_type": "image/png"
}Request Body Fields:
filename(string, required) - Original filenamecontent_type(string, required) - MIME type- For screenshots:
image/png,image/jpeg - For traces:
application/json
- For screenshots:
Response:
{
"artifact_id": "01933d7e-8f2a-7890-abcd-ef1234567890",
"upload_url": "https://s3.amazonaws.com/bucket/artifacts/usecase-123/executions/execution-456/steps/step-789/screenshot.png?X-Amz-Algorithm=...",
"expires_in": 3600,
"s3_key": "artifacts/usecase-123/executions/execution-456/steps/step-789/screenshot.png"
}Error Responses:
400 Bad Request- Missing fields or invalid content type401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes404 Not Found- Execution or step not found500 Internal Server Error- Server error
Example (cURL):
# Step 1: Get presigned URL
RESPONSE=$(curl -X POST \
'https://api.example.com/usecase/usecase-123/executions/execution-456/steps/step-789/artifacts' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"filename": "screenshot.png",
"content_type": "image/png"
}')
UPLOAD_URL=$(echo $RESPONSE | jq -r '.upload_url')
# Step 2: Upload file to S3 using presigned URL
curl -X PUT "$UPLOAD_URL" \
-H 'Content-Type: image/png' \
--data-binary @screenshot.pngExample (Python):
import requests
# Step 1: Get presigned URL
response = requests.post(
f'https://api.example.com/usecase/{usecase_id}/executions/{execution_id}/steps/{step_id}/artifacts',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={
'filename': 'screenshot.png',
'content_type': 'image/png'
}
)
data = response.json()
upload_url = data['upload_url']
# Step 2: Upload file to S3
with open('screenshot.png', 'rb') as f:
upload_response = requests.put(
upload_url,
headers={'Content-Type': 'image/png'},
data=f
)
print(f"Upload status: {upload_response.status_code}")Example (JavaScript):
// Step 1: Get presigned URL
const response = await fetch(
`https://api.example.com/usecase/${usecaseId}/executions/${executionId}/steps/${stepId}/artifacts`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
filename: 'screenshot.png',
content_type: 'image/png'
})
}
);
const data = await response.json();
// Step 2: Upload file to S3
const uploadResponse = await fetch(data.upload_url, {
method: 'PUT',
headers: {
'Content-Type': 'image/png'
},
body: screenshotBlob
});
console.log(`Upload status: ${uploadResponse.status}`);Request an asynchronous download of the Device Farm session recording for a mobile execution. The recording is downloaded from Device Farm and uploaded to S3 after a 5-minute delay (to allow Device Farm to finalize the session).
Endpoint: POST /usecase/{id}/executions/{executionId}/download-recording
Path Parameters:
id(string, required) - Usecase IDexecutionId(string, required) - Execution ID
Required Scopes: api/executions.write or api/admin
Request Body:
{
"session_arn": "arn:aws:devicefarm:us-west-2:123456789:session:project-id/session-id/00000",
"nova_session_id": "019d1234-5678-abcd-ef01-234567890abc"
}Request Body Fields:
session_arn(string, required) - Device Farm remote access session ARNnova_session_id(string, optional) - Nova Act session ID (defaults to execution ID)
Response (200 OK):
{
"message": "Recording download requested",
"delay_seconds": 300
}Example (cURL):
curl -X POST \
'https://api.example.com/api/usecase/usecase-123/executions/execution-456/download-recording' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"session_arn": "arn:aws:devicefarm:us-west-2:123456789:session:project-id/session-id/00000"
}'Upsert a single captured runtime variable on an in-flight execution. Called by the CLI runner once per retrieve_value or transform step so the UI reflects captured values incrementally rather than only at completion.
Endpoint: POST /api/usecase/{id}/executions/{executionId}/runtime-variables
Scope: api/executions.write
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
string | Usecase ID |
executionId |
string | Execution ID |
Request Body:
{
"key": "orderId",
"value": "ORD-42"
}| Field | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Variable name (≤128 chars, may be a DynamoDB reserved word) |
value |
string / number / bool | Yes | Variable value. Coerced to string on storage (≤4096 chars) |
Responses:
| Status | Meaning |
|---|---|
| 200 | {"status": "ok", "key": "<key>"} |
| 400 | Missing key/value, oversized value, or invalid body |
| 403 | Missing api/executions.write scope |
| 404 | Execution variables record not found |
| 500 | DynamoDB error |
The live-view URL is the user-visible streaming URL of a running AgentCore browser session. The runner publishes it at provisioning time; the frontend polls GET and the runner cleans up with DELETE on teardown.
Endpoint: POST /api/usecase/{id}/executions/{executionId}/live-view
Scope: api/executions.write
Request Body:
{
"live_view_url": "https://live.example.com/session-abc"
}| Field | Type | Required | Description |
|---|---|---|---|
live_view_url |
string | Yes | HTTPS URL of the live view, ≤2048 chars |
Responses: 200 ({"status": "ok"}), 400 (bad URL), 403, 500.
Endpoint: DELETE /api/usecase/{id}/executions/{executionId}/live-view
Scope: api/executions.write
Responses:
| Status | Meaning |
|---|---|
| 204 | Deleted |
| 403 | Missing scope |
| 404 | No live-view record to delete (callers may treat as already-clean) |
| 500 | DynamoDB error |
Partial update for Device Farm session metadata on a mobile execution. Called by the runner right after the session starts (session ARN only) and again after the session stops (final ARN + optional device details).
Endpoint: PATCH /api/usecase/{id}/executions/{executionId}/mobile-metadata
Scope: api/executions.write
Request Body (at least one field required; missing fields are NOT cleared server-side):
{
"device_farm_session_arn": "arn:aws:devicefarm:us-west-2:123456789012:session:abcdef",
"device_name": "Pixel 6",
"device_os_version": "Android 13"
}| Field | Type | Required | Description |
|---|---|---|---|
device_farm_session_arn |
string | No | Must start with arn:aws:devicefarm: |
device_name |
string | No | ≤512 chars |
device_os_version |
string | No | ≤512 chars |
Responses: 200 ({"status": "ok", "updated_fields": [...]}), 400, 403, 404 (execution not found), 500.
Read-only access to custom HTTP headers stored on an execution. The runner reads these and calls nova.page.set_extra_http_headers(...) before navigation. Returns {"headers": {}} (not 404) when no headers are configured.
Endpoint: GET /api/usecase/{id}/executions/{executionId}/headers
Scope: api/executions.read
Response:
{
"headers": {
"X-Custom": "value",
"Authorization": "Bearer …"
}
}Presigned S3 URLs for per-step trajectory JSONs used by Nova Act's trajectory-replay cache.
Endpoint: POST /api/usecase/{id}/steps/{stepId}/trajectory/upload-url
Scope: api/executions.write
Request Body:
{
"content_type": "application/json"
}Response:
{
"upload_url": "https://s3.example.com/…",
"s3_key": "uc-1/trajectories/step-1.json",
"expires_in": 900
}Calling this endpoint atomically writes trajectory_s3_key and trajectory_last_updated on the STEP record server-side. If the runner's subsequent PUT fails, the pointer is stale; the next replay attempt triggers cleanup via the clear_cache_fields extension on the step-status endpoint.
Endpoint: GET /api/usecase/{id}/steps/{stepId}/trajectory/download-url
Scope: api/executions.write (cache data is execution-flow-sensitive)
Response (200):
{
"download_url": "https://s3.example.com/…",
"expires_in": 900
}Response (404): when no trajectory has been recorded for the step — expected when the step hasn't been executed yet.
The existing PATCH /api/usecase/{id}/executions/{executionId}/steps/{stepId}/status endpoint accepts an optional clear_cache_fields array that lets the runner clean up stale cache pointers in the same request.
Allow-listed field names (anything else rejected 400):
trajectory_s3_keytrajectory_last_updatedcached_stepscache_last_updated
Example:
{
"status": "failed",
"error_message": "step timed out",
"clear_cache_fields": ["trajectory_s3_key", "trajectory_last_updated"]
}The server does a combined SET status = …, REMOVE trajectory_s3_key, trajectory_last_updated in a single DynamoDB update. No extra round-trip.
List available AWS Device Farm devices for mobile testing. Returns devices with remote access enabled, filtered by platform.
Endpoint: GET /devices
Query Parameters:
platform(string, optional) - Filter by platform:"ANDROID"or"IOS"
Required Scopes: api/usecases.read or api/admin
Response (200 OK):
{
"devices": [
{
"arn": "arn:aws:devicefarm:us-west-2::device:ABC123",
"name": "Apple iPhone 15",
"platform": "IOS",
"os": "18.0",
"formFactor": "PHONE",
"manufacturer": "Apple",
"modelId": "iPhone15",
"availability": "HIGHLY_AVAILABLE"
}
]
}Response Fields:
arn- Device Farm device ARN (use this asdevice_arnwhen creating a usecase)name- Device display nameplatform-"ANDROID"or"IOS"os- Operating system versionformFactor-"PHONE"or"TABLET"manufacturer- Device manufactureravailability-"HIGHLY_AVAILABLE","AVAILABLE", or"BUSY"
Example (cURL):
curl -X GET \
'https://api.example.com/api/devices?platform=IOS' \
-H 'Authorization: Bearer <token>'Create a suite execution record and execution records for all usecases in a test suite with overrides applied. This endpoint is designed for CI/CD runners and does not spawn ECS tasks.
Endpoint: POST /test-suites/{id}/execute
Path Parameters:
id(string, required) - Test suite UUID
Required Scopes: api/suite.write, api/execution.write
Request Body:
{
"trigger_type": "ci_runner",
"base_url": "https://example.com",
"variables": {
"username": "testuser",
"api_key": "secret123"
},
"region": "us-east-1",
"model_id": "us.amazon.nova-2-lite-v1:0"
}Request Body Schema:
trigger_type(string, required) - Must be "ci_runner"base_url(string, optional) - Base URL override for all usecases. Replaces domain/origin while preserving path and query parametersvariables(object, optional) - Variable overrides (key-value pairs). Precedence: CLI > usecase > secretsregion(string, optional) - AWS region override for all executionsmodel_id(string, optional) - Bedrock model ID override for all executions
Response (200 OK):
{
"suite_execution_id": "01234567-89ab-cdef-0123-456789abcdef",
"suite_id": "01234567-89ab-cdef-0123-456789abcdef",
"status": "pending",
"created_at": "2024-02-16T12:00:00Z",
"execution_ids": [
{
"usecase_id": "01234567-89ab-cdef-0123-456789abcdef",
"execution_id": "01234567-89ab-cdef-0123-456789abcdef",
"usecase_name": "Login with valid credentials"
},
{
"usecase_id": "01234567-89ab-cdef-0123-456789abcdef",
"execution_id": "01234567-89ab-cdef-0123-456789abcdef",
"usecase_name": "Login with invalid password"
}
]
}Error Responses:
400 Bad Request - Invalid input or unresolved variables:
{
"error": "Unresolved variables",
"message": "Unresolved variables: username, password",
"details": {
"usecase_id": "01234567-89ab-cdef-0123-456789abcdef",
"usecase_name": "Login test"
}
}403 Forbidden - Insufficient permissions:
{
"error": "Forbidden",
"message": "Missing required scopes: api/suite.write, api/execution.write",
"required_scopes": ["api/suite.write", "api/execution.write"],
"token_scopes": ["api/suite.read"]
}404 Not Found - Test suite not found:
{
"error": "Test suite not found",
"message": "No test suite found with ID: 01234567-89ab-cdef-0123-456789abcdef"
}500 Internal Server Error - Database or system error:
{
"error": "Failed to execute test suite",
"message": "Error details"
}Example (cURL):
curl -X POST \
'https://api.example.com/test-suites/01234567-89ab-cdef-0123-456789abcdef/execute' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"trigger_type": "ci_runner",
"base_url": "https://production.example.com",
"variables": {
"username": "prod_user",
"api_key": "prod_key_123"
},
"region": "us-west-2",
"model_id": "us.amazon.nova-2-lite-v1:0"
}'Example (Python):
import requests
response = requests.post(
'https://api.example.com/test-suites/01234567-89ab-cdef-0123-456789abcdef/execute',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
},
json={
'trigger_type': 'ci_runner',
'base_url': 'https://production.example.com',
'variables': {
'username': 'prod_user',
'api_key': 'prod_key_123'
},
'region': 'us-west-2',
'model_id': 'us.amazon.nova-2-lite-v1:0'
}
)
data = response.json()
suite_execution_id = data['suite_execution_id']
execution_ids = data['execution_ids']
print(f"Suite execution created: {suite_execution_id}")
print(f"Created {len(execution_ids)} executions")Example (JavaScript):
const response = await fetch(
'https://api.example.com/test-suites/01234567-89ab-cdef-0123-456789abcdef/execute',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
trigger_type: 'ci_runner',
base_url: 'https://production.example.com',
variables: {
username: 'prod_user',
api_key: 'prod_key_123'
},
region: 'us-west-2',
model_id: 'us.amazon.nova-2-lite-v1:0'
})
}
);
const data = await response.json();
console.log(`Suite execution created: ${data.suite_execution_id}`);
console.log(`Created ${data.execution_ids.length} executions`);Notes:
- No ECS tasks are spawned when using
trigger_type=ci_runner - All execution records are created with
status=pending - Base URL override replaces only the scheme and domain, preserving path and query parameters
- Variable merge precedence: CLI overrides > usecase variables > secrets
- Maximum recommended suite size: 10 usecases (to avoid API Gateway timeout)
OAuth clients enable machine-to-machine (M2M) authentication for CI/CD pipelines and automated systems. Clients use the OAuth 2.0 client credentials flow and are scoped to specific API permissions.
Create a new OAuth client with specified scopes for CI/CD authentication.
Endpoint: POST /api/oauth-clients
Required Scopes: api/oauth-clients.write or api/admin
Request Body:
{
"name": "CI/CD Runner - Production",
"scopes": [
"api/suite.read",
"api/suite.write",
"api/execution.write"
]
}Request Body Fields:
name(string, required) - Client name for identificationscopes(array, required) - List of OAuth scopes to grant- User can only grant scopes they possess (unless admin)
- Valid scopes:
api/suite.read,api/suite.write,api/execution.read,api/execution.write,api/oauth-clients.read,api/oauth-clients.write,api/admin
Response (201 Created):
{
"client_id": "7abc123def456",
"client_secret": "secret_xyz789...",
"client_name": "CI/CD Runner - Production",
"scopes": [
"api/suite.read",
"api/suite.write",
"api/execution.write"
],
"created_date": "2026-02-16T12:00:00Z",
"created_by": "user@example.com",
"refresh_token_validity": 30,
"access_token_validity": 60,
"id_token_validity": 60
}Response Fields:
client_id- Unique client identifier for authenticationclient_secret- Client secret (ONLY shown once, cannot be retrieved later)client_name- Client namescopes- Granted OAuth scopescreated_date- ISO8601 timestamp of creationcreated_by- User who created the client- Token validity fields in minutes/days
Error Responses:
400 Bad Request- Invalid scopes, missing name, or invalid request format401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes or privilege escalation attempt429 Too Many Requests- Too many OAuth clients500 Internal Server Error- Server error
Example (cURL):
curl -X POST \
'https://api.example.com/api/oauth-clients' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "CI/CD Runner - Production",
"scopes": [
"api/suite.read",
"api/suite.write",
"api/execution.write"
]
}'Example (Python):
import requests
response = requests.post(
'https://api.example.com/api/oauth-clients',
headers={
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
},
json={
'name': 'CI/CD Runner - Production',
'scopes': [
'api/suite.read',
'api/suite.write',
'api/execution.write'
]
}
)
data = response.json()
client_id = data['client_id']
client_secret = data['client_secret'] # Save this - it won't be shown again!
print(f"Client ID: {client_id}")
print(f"Client Secret: {client_secret}")Example (JavaScript):
const response = await fetch('https://api.example.com/api/oauth-clients', {
method: 'POST',
headers: {
'Authorization': `Bearer ${jwtToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'CI/CD Runner - Production',
scopes: [
'api/suite.read',
'api/suite.write',
'api/execution.write'
]
})
});
const data = await response.json();
console.log(`Client ID: ${data.client_id}`);
console.log(`Client Secret: ${data.client_secret}`); // Save this!Security Notes:
- Client secret is ONLY returned at creation - save it securely
- Users cannot grant scopes they don't possess (prevents privilege escalation)
- Admin users can grant any scope
List all OAuth clients in the user pool with metadata.
Endpoint: GET /api/oauth-clients
Required Scopes: api/oauth-clients.read or api/admin
Response (200 OK):
{
"clients": [
{
"client_id": "7abc123def456",
"client_name": "CI/CD Runner - Production",
"created_date": "2026-02-16T12:00:00Z",
"last_modified_date": "2026-02-16T12:00:00Z",
"created_by": "user@example.com",
"allowed_oauth_scopes": [
"api/suite.read",
"api/suite.write",
"api/execution.write"
],
"refresh_token_validity": 30,
"access_token_validity": 60,
"id_token_validity": 60,
"enabled": true
}
],
"count": 1
}Error Responses:
401 Unauthorized- Missing or invalid token403 Forbidden- Insufficient scopes500 Internal Server Error- Server error
Example (cURL):
curl -X GET \
'https://api.example.com/api/oauth-clients' \
-H 'Authorization: Bearer <jwt_token>'Example (Python):
import requests
response = requests.get(
'https://api.example.com/api/oauth-clients',
headers={'Authorization': f'Bearer {jwt_token}'}
)
data = response.json()
print(f"Found {data['count']} OAuth clients")
for client in data['clients']:
print(f" - {client['client_name']} ({client['client_id']})")Example (JavaScript):
const response = await fetch('https://api.example.com/api/oauth-clients', {
headers: {
'Authorization': `Bearer ${jwtToken}`
}
});
const data = await response.json();
console.log(`Found ${data.count} OAuth clients`);
data.clients.forEach(client => {
console.log(` - ${client.client_name} (${client.client_id})`);
});Security Notes:
- Client secrets are NEVER returned in list responses
- Only shows clients created through the application (with metadata)
Delete an OAuth client, immediately revoking its credentials.
Endpoint: DELETE /api/oauth-clients/{clientId}
Path Parameters:
clientId(string, required) - OAuth client ID to delete
Required Scopes: api/oauth-clients.write or api/admin
Response (200 OK):
{
"message": "OAuth client deleted successfully",
"client_id": "7abc123def456"
}Error Responses:
400 Bad Request- Missing client ID401 Unauthorized- Missing or invalid token403 Forbidden- Client not created through application or insufficient permissions404 Not Found- Client not found500 Internal Server Error- Server error
Example (cURL):
curl -X DELETE \
'https://api.example.com/api/oauth-clients/7abc123def456' \
-H 'Authorization: Bearer <jwt_token>'Example (Python):
import requests
response = requests.delete(
'https://api.example.com/api/oauth-clients/7abc123def456',
headers={'Authorization': f'Bearer {jwt_token}'}
)
if response.status_code == 200:
print("OAuth client deleted successfully")Example (JavaScript):
const response = await fetch(
'https://api.example.com/api/oauth-clients/7abc123def456',
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${jwtToken}`
}
}
);
if (response.ok) {
console.log('OAuth client deleted successfully');
}Security Notes:
- Deletion is immediate - client cannot authenticate after deletion
- Only clients with metadata (created through application) can be deleted
- Users can only delete clients they created (unless admin)
Generate a new client secret by recreating the OAuth client. Old secret is immediately invalidated.
Endpoint: POST /api/oauth-clients/{clientId}/rotate-secret
Path Parameters:
clientId(string, required) - OAuth client ID to rotate
Required Scopes: api/oauth-clients.write or api/admin
Response (200 OK):
{
"client_id": "8def456ghi789",
"client_secret": "new_secret_abc123...",
"rotated_at": "2026-02-16T15:00:00Z"
}Response Fields:
client_id- New client ID (changes during rotation)client_secret- New client secret (ONLY shown once, cannot be retrieved later)rotated_at- ISO8601 timestamp of rotation
Error Responses:
400 Bad Request- Missing client ID401 Unauthorized- Missing or invalid token403 Forbidden- User doesn't own client or insufficient permissions404 Not Found- Client not found500 Internal Server Error- Server error
Example (cURL):
curl -X POST \
'https://api.example.com/api/oauth-clients/7abc123def456/rotate-secret' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json'Example (Python):
import requests
response = requests.post(
'https://api.example.com/api/oauth-clients/7abc123def456/rotate-secret',
headers={
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
}
)
data = response.json()
new_client_id = data['client_id']
new_client_secret = data['client_secret'] # Save this - it won't be shown again!
print(f"New Client ID: {new_client_id}")
print(f"New Client Secret: {new_client_secret}")
print(f"Rotated at: {data['rotated_at']}")Example (JavaScript):
const response = await fetch(
'https://api.example.com/api/oauth-clients/7abc123def456/rotate-secret',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${jwtToken}`,
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
console.log(`New Client ID: ${data.client_id}`);
console.log(`New Client Secret: ${data.client_secret}`); // Save this!
console.log(`Rotated at: ${data.rotated_at}`);Security Notes:
- Old secret is immediately invalidated - update all systems using the client
- Client ID changes during rotation - update configuration accordingly
- New secret is ONLY shown once - save it securely
- Users can only rotate secrets for clients they created (unless admin)
Implementation Notes:
- Cognito doesn't support in-place secret rotation
- Rotation deletes and recreates the app client with same settings
- All configuration (scopes, token validity) is preserved
All error responses follow this consistent format:
{
"error": "Error type or category",
"message": "Detailed error description",
"required_scopes": ["api/execution.write"],
"token_scopes": ["api/execution.read"]
}Error Response Fields:
error- Short error type or categorymessage- Detailed human-readable error descriptionrequired_scopes(optional) - Scopes required for the operation (403 errors)token_scopes(optional) - Scopes present in the token (403 errors)details(optional) - Additional context-specific error details
| Status Code | Description | Common Causes |
|---|---|---|
200 OK |
Request succeeded | Successful GET, PATCH, POST operations |
201 Created |
Resource created successfully | Successful POST operations creating new resources |
204 No Content |
Successful deletion | Successful DELETE operations |
400 Bad Request |
Invalid input | Missing required fields, invalid values, malformed JSON |
401 Unauthorized |
Authentication failed | Missing token, expired token, invalid token |
403 Forbidden |
Insufficient permissions | Missing required scopes, privilege escalation attempt |
404 Not Found |
Resource not found | Invalid ID, deleted resource, wrong endpoint |
429 Too Many Requests |
Rate limit exceeded | Too many requests in short time period |
500 Internal Server Error |
Server error | Database errors, AWS service errors, unexpected exceptions |
Missing Authorization Header:
{
"error": "Unauthorized",
"message": "Missing Authorization header"
}Invalid or Expired Token:
{
"error": "Unauthorized",
"message": "Invalid or expired token"
}Solution: Obtain a new access token using the OAuth 2.0 client credentials flow.
Insufficient Scopes:
{
"error": "Forbidden",
"message": "Missing required scopes: api/suite.write, api/execution.write",
"required_scopes": ["api/suite.write", "api/execution.write"],
"token_scopes": ["api/suite.read"]
}Solution: Request an OAuth client with the required scopes or use a token with sufficient permissions.
Privilege Escalation Attempt:
{
"error": "Forbidden",
"message": "Cannot grant scopes that you don't possess: api/admin"
}Solution: Only grant scopes that your user account possesses, or contact an administrator.
Missing Required Fields:
{
"error": "Bad Request",
"message": "Missing required field: name"
}Invalid Field Values:
{
"error": "Bad Request",
"message": "Invalid status value: invalid_status. Valid values: pending, running, completed, failed, skipped"
}Unresolved Variables:
{
"error": "Unresolved variables",
"message": "Unresolved variables: username, password",
"details": {
"usecase_id": "01234567-89ab-cdef-0123-456789abcdef",
"usecase_name": "Login test"
}
}Solution: Provide all required variables via the variables parameter.
{
"error": "Test suite not found",
"message": "No test suite found with ID: 01234567-89ab-cdef-0123-456789abcdef"
}Solution: Verify the resource ID is correct and the resource exists.
{
"error": "Too Many Requests",
"message": "Rate limit exceeded. Please retry after 60 seconds."
}Solution: Implement exponential backoff and retry logic in your client.
API Gateway enforces the following rate limits to ensure fair usage and system stability:
Limits:
- Burst Capacity: 5,000 requests
- Steady-State Rate: 10,000 requests per second
Behavior:
- Requests exceeding the burst capacity are throttled
- Throttled requests receive a
429 Too Many Requestsresponse - Rate limits apply per AWS account
Best Practices:
- Implement exponential backoff for retry logic
- Cache responses when appropriate
- Batch operations when possible
- Monitor your request rate and adjust accordingly
Example Retry Logic (Python):
import requests
import time
def api_request_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
# Exponential backoff: 1s, 2s, 4s
wait_time = 2 ** attempt
print(f"Rate limited. Retrying in {wait_time}s...")
time.sleep(wait_time)
continue
return response
raise Exception("Max retries exceeded")Example Retry Logic (JavaScript):
async function apiRequestWithRetry(url, headers, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, { headers });
if (response.status === 429) {
// Exponential backoff: 1s, 2s, 4s
const waitTime = Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Retrying in ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}Currently, the API does not implement pagination for list endpoints. All results are returned in a single response.
Future Enhancement: Pagination will be added in a future version using cursor-based pagination:
{
"items": [...],
"next_cursor": "eyJsYXN0X2lkIjogIjEyMyJ9",
"has_more": true
}Recommended Limits:
- OAuth clients list: Returns all clients (typically < 100)
- Test suite executions: Consider filtering by date range in future versions
This example demonstrates a complete CI/CD workflow using the API, from authentication to test execution and artifact upload.
#!/bin/bash
set -e
# Configuration
COGNITO_DOMAIN="your-user-pool-domain"
REGION="us-east-1"
API_BASE_URL="https://api.example.com"
CLIENT_ID="${OAUTH_CLIENT_ID}"
CLIENT_SECRET="${OAUTH_CLIENT_SECRET}"
SUITE_ID="01234567-89ab-cdef-0123-456789abcdef"
echo "=== CI/CD Test Execution Workflow ==="
# Step 1: Obtain access token
echo "Step 1: Obtaining access token..."
TOKEN_RESPONSE=$(curl -s -X POST \
"https://${COGNITO_DOMAIN}.auth.${REGION}.amazoncognito.com/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials' \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d 'scope=api/suite.read api/suite.write api/execution.read api/execution.write')
TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then
echo "ERROR: Failed to obtain access token"
echo $TOKEN_RESPONSE
exit 1
fi
echo "✓ Access token obtained"
# Step 2: Execute test suite
echo "Step 2: Executing test suite..."
SUITE_RESPONSE=$(curl -s -X POST \
"${API_BASE_URL}/test-suites/${SUITE_ID}/execute" \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
-d '{
"trigger_type": "ci_runner",
"base_url": "https://staging.example.com",
"variables": {
"username": "testuser",
"password": "testpass"
}
}')
SUITE_EXECUTION_ID=$(echo $SUITE_RESPONSE | jq -r '.suite_execution_id')
EXECUTION_IDS=$(echo $SUITE_RESPONSE | jq -r '.execution_ids')
echo "✓ Suite execution created: ${SUITE_EXECUTION_ID}"
echo "✓ Created $(echo $EXECUTION_IDS | jq length) executions"
# Step 3: Execute each usecase and report progress
echo "Step 3: Executing usecases..."
echo $EXECUTION_IDS | jq -c '.[]' | while read execution; do
USECASE_ID=$(echo $execution | jq -r '.usecase_id')
EXECUTION_ID=$(echo $execution | jq -r '.execution_id')
USECASE_NAME=$(echo $execution | jq -r '.usecase_name')
echo " Executing: ${USECASE_NAME}"
# Get execution steps
STEPS=$(curl -s -X GET \
"${API_BASE_URL}/usecase/${USECASE_ID}/executions/${EXECUTION_ID}/steps" \
-H "Authorization: Bearer ${TOKEN}")
# Execute each step
echo $STEPS | jq -c '.[]' | while read step; do
STEP_ID=$(echo $step | jq -r '.id')
STEP_NAME=$(echo $step | jq -r '.name')
echo " Step: ${STEP_NAME}"
# Mark step as running
curl -s -X PATCH \
"${API_BASE_URL}/usecase/${USECASE_ID}/executions/${EXECUTION_ID}/steps/${STEP_ID}/status" \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
-d "{
\"status\": \"running\",
\"started_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}" > /dev/null
# Execute step logic here
# ... your test execution code ...
# Upload screenshot if available
if [ -f "screenshot_${STEP_ID}.png" ]; then
ARTIFACT_RESPONSE=$(curl -s -X POST \
"${API_BASE_URL}/usecase/${USECASE_ID}/executions/${EXECUTION_ID}/steps/${STEP_ID}/artifacts" \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
-d '{
"filename": "screenshot.png",
"content_type": "image/png"
}')
UPLOAD_URL=$(echo $ARTIFACT_RESPONSE | jq -r '.upload_url')
curl -s -X PUT "$UPLOAD_URL" \
-H 'Content-Type: image/png' \
--data-binary @"screenshot_${STEP_ID}.png" > /dev/null
echo " ✓ Screenshot uploaded"
fi
# Mark step as completed
curl -s -X PATCH \
"${API_BASE_URL}/usecase/${USECASE_ID}/executions/${EXECUTION_ID}/steps/${STEP_ID}/status" \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
-d "{
\"status\": \"completed\",
\"completed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}" > /dev/null
echo " ✓ Step completed"
done
# Upload execution-level artifacts
if [ -f "recording_${EXECUTION_ID}.webm" ]; then
echo " Uploading recording..."
ARTIFACT_RESPONSE=$(curl -s -X POST \
"${API_BASE_URL}/usecase/${USECASE_ID}/executions/${EXECUTION_ID}/artifacts" \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
-d '{
"type": "recording",
"filename": "recording.webm",
"content_type": "video/webm"
}')
UPLOAD_URL=$(echo $ARTIFACT_RESPONSE | jq -r '.upload_url')
curl -s -X PUT "$UPLOAD_URL" \
-H 'Content-Type: video/webm' \
--data-binary @"recording_${EXECUTION_ID}.webm" > /dev/null
echo " ✓ Recording uploaded"
fi
echo " ✓ Usecase completed: ${USECASE_NAME}"
done
echo "=== Workflow completed successfully ==="import requests
import time
from datetime import datetime
from typing import Dict, List
class NovaActCICDClient:
def __init__(self, cognito_domain: str, region: str, api_base_url: str,
client_id: str, client_secret: str):
self.cognito_domain = cognito_domain
self.region = region
self.api_base_url = api_base_url
self.client_id = client_id
self.client_secret = client_secret
self.access_token = None
self.token_expiry = 0
def get_access_token(self) -> str:
"""Obtain access token using OAuth 2.0 client credentials flow."""
# Check if token is still valid
if self.access_token and time.time() < self.token_expiry:
return self.access_token
token_url = f"https://{self.cognito_domain}.auth.{self.region}.amazoncognito.com/oauth2/token"
response = requests.post(
token_url,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data={
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret,
'scope': 'api/suite.read api/suite.write api/execution.read api/execution.write'
}
)
response.raise_for_status()
data = response.json()
self.access_token = data['access_token']
self.token_expiry = time.time() + data['expires_in'] - 60 # Refresh 1 min early
return self.access_token
def execute_test_suite(self, suite_id: str, base_url: str = None,
variables: Dict[str, str] = None) -> Dict:
"""Execute a test suite with optional overrides."""
token = self.get_access_token()
payload = {'trigger_type': 'ci_runner'}
if base_url:
payload['base_url'] = base_url
if variables:
payload['variables'] = variables
response = requests.post(
f"{self.api_base_url}/test-suites/{suite_id}/execute",
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
json=payload
)
response.raise_for_status()
return response.json()
def update_step_status(self, usecase_id: str, execution_id: str,
step_id: str, status: str, error_message: str = None):
"""Update the status of an execution step."""
token = self.get_access_token()
payload = {
'status': status,
'started_at': datetime.utcnow().isoformat() + 'Z'
}
if status in ['completed', 'failed']:
payload['completed_at'] = datetime.utcnow().isoformat() + 'Z'
if error_message:
payload['error_message'] = error_message
response = requests.patch(
f"{self.api_base_url}/usecase/{usecase_id}/executions/{execution_id}/steps/{step_id}/status",
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
json=payload
)
response.raise_for_status()
return response.json()
def upload_step_artifact(self, usecase_id: str, execution_id: str,
step_id: str, file_path: str, content_type: str):
"""Upload a step artifact (screenshot, trace)."""
token = self.get_access_token()
# Get presigned URL
response = requests.post(
f"{self.api_base_url}/usecase/{usecase_id}/executions/{execution_id}/steps/{step_id}/artifacts",
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
json={
'filename': file_path.split('/')[-1],
'content_type': content_type
}
)
response.raise_for_status()
data = response.json()
upload_url = data['upload_url']
# Upload file to S3
with open(file_path, 'rb') as f:
upload_response = requests.put(
upload_url,
headers={'Content-Type': content_type},
data=f
)
upload_response.raise_for_status()
return data['artifact_id']
def upload_execution_artifact(self, usecase_id: str, execution_id: str,
artifact_type: str, file_path: str,
content_type: str):
"""Upload an execution artifact (recording, logs)."""
token = self.get_access_token()
# Get presigned URL
response = requests.post(
f"{self.api_base_url}/usecase/{usecase_id}/executions/{execution_id}/artifacts",
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
json={
'type': artifact_type,
'filename': file_path.split('/')[-1],
'content_type': content_type
}
)
response.raise_for_status()
data = response.json()
upload_url = data['upload_url']
# Upload file to S3
with open(file_path, 'rb') as f:
upload_response = requests.put(
upload_url,
headers={'Content-Type': content_type},
data=f
)
upload_response.raise_for_status()
return data['artifact_id']
# Usage example
if __name__ == '__main__':
client = NovaActCICDClient(
cognito_domain='your-user-pool-domain',
region='us-east-1',
api_base_url='https://api.example.com',
client_id='7abc123def456',
client_secret='secret_xyz789...'
)
# Execute test suite
result = client.execute_test_suite(
suite_id='01234567-89ab-cdef-0123-456789abcdef',
base_url='https://staging.example.com',
variables={'username': 'testuser', 'password': 'testpass'}
)
print(f"Suite execution created: {result['suite_execution_id']}")
print(f"Created {len(result['execution_ids'])} executions")
# Process each execution
for execution in result['execution_ids']:
usecase_id = execution['usecase_id']
execution_id = execution['execution_id']
print(f"Executing: {execution['usecase_name']}")
# Execute steps and update status
# ... your test execution logic ...
# Upload artifacts
client.upload_execution_artifact(
usecase_id, execution_id,
'recording', 'recording.webm', 'video/webm'
)Current Version: 1.2.0
The API uses semantic versioning (MAJOR.MINOR.PATCH):
- MAJOR: Breaking changes that require client updates
- MINOR: New features, backward compatible
- PATCH: Bug fixes, backward compatible
Version History:
- Added OAuth client management endpoints
- Added client credentials flow support for M2M authentication
- Added scope validation and privilege escalation prevention
- Added client secret rotation capability
- Added test suite execution endpoint for CI/CD
- Added support for base URL, variable, region, and model overrides
- Added structured logging and CloudWatch metrics
- Initial release with execution endpoints
- Added
ci_runnertrigger type - Added step status update endpoint
- Added artifact presigned URL endpoints
Deprecation Policy:
- Deprecated endpoints will be supported for at least 6 months
- Deprecation notices will be included in API responses
- Breaking changes will increment the major version
- Least Privilege: Grant only the minimum scopes required
- Secret Storage: Store client secrets in secure secret managers (AWS Secrets Manager, HashiCorp Vault)
- Secret Rotation: Rotate client secrets regularly (recommended: every 90 days)
- Access Review: Regularly review and remove unused OAuth clients
- Monitoring: Monitor OAuth client usage and authentication failures
- Token Expiry: Access tokens expire after 1 hour - implement automatic refresh
- Secure Storage: Never commit tokens or credentials to version control
- Environment Variables: Use environment variables or secret managers for credentials
- Token Validation: Validate token expiry before making API requests
- HTTPS Only: Always use HTTPS for API requests
- Input Validation: Validate and sanitize all input data
- Error Handling: Don't expose sensitive information in error messages
- Rate Limiting: Implement client-side rate limiting to avoid throttling
- Logging: Log API requests for audit purposes (exclude sensitive data)
- IP Whitelisting: Consider restricting API access to known IP ranges
- VPC Endpoints: Use VPC endpoints for internal AWS service communication
- WAF Rules: Implement AWS WAF rules for additional protection
- DDoS Protection: Use AWS Shield for DDoS protection
- Installation Guide
- Configuration Reference
- CLI Reference
- CI/CD Integration Examples
- Troubleshooting Guide
- Best Practices
- GitHub Issues: https://github.com/aws-samples/sample-nova-act-qa-studio/issues
- Documentation: https://github.com/aws-samples/sample-nova-act-qa-studio
If you discover a security vulnerability, please email security@example.com instead of using the public issue tracker.
Added:
- OAuth client management endpoints (
POST /api/oauth-clients,GET /api/oauth-clients,DELETE /api/oauth-clients/{clientId},POST /api/oauth-clients/{clientId}/rotate-secret) - OAuth 2.0 client credentials flow documentation
- Scope validation and privilege escalation prevention
- Client secret rotation capability
- Complete CI/CD workflow examples in Bash and Python
Security:
- Client secrets only shown once at creation/rotation
- Immediate credential revocation on deletion
- Ownership verification for client management operations
- Audit logging for all OAuth client operations
Added:
- Test suite execution endpoint (
POST /test-suites/{id}/execute) - Support for base URL overrides
- Support for variable overrides with precedence
- Support for region and model_id overrides
- Structured logging and CloudWatch metrics
Initial Release:
- Execute usecase endpoint with
ci_runnertrigger type - Update execution step status endpoint
- Generate execution artifact presigned URL endpoint
- Generate step artifact presigned URL endpoint
- M2M token authentication support
| Scope | Resource | Permission | Description |
|---|---|---|---|
api/admin |
All | Full | Full access to all resources |
api/execution.write |
Executions | Write | Create and update executions |
api/execution.read |
Executions | Read | Read execution data |
api/usecases.write |
Usecases | Write | Create and update usecases |
api/usecases.read |
Usecases | Read | Read usecase data |
api/suite.write |
Test Suites | Write | Manage test suites |
api/suite.read |
Test Suites | Read | Read test suite data |
api/oauth-clients.write |
OAuth Clients | Write | Create, delete, and rotate OAuth clients |
api/oauth-clients.read |
OAuth Clients | Read | List OAuth clients |
| Status | Description | Terminal State |
|---|---|---|
pending |
Step has not started | No |
running |
Step is currently executing | No |
completed |
Step completed successfully | Yes |
failed |
Step failed with error | Yes |
skipped |
Step was skipped | Yes |
Execution-Level Artifacts:
| Type | Content Types | Description |
|---|---|---|
recording |
video/webm, video/mp4 |
Video recording of execution |
logs |
text/plain |
Execution logs |
Step-Level Artifacts:
| Type | Content Types | Description |
|---|---|---|
| Screenshot | image/png, image/jpeg |
Screenshot of step |
| Trace | application/json |
Playwright trace file |
| Trigger Type | Description | ECS Task | Use Case |
|---|---|---|---|
OnDemand |
Queue for worker | No | Default execution |
Scheduled |
Direct ECS task | Yes | EventBridge scheduled |
OnDemandHeadless |
Direct ECS task | Yes | UI-triggered execution |
ci_runner |
Record only | No | CI/CD integration |
Last Updated: 2024-02-16
API Version: 1.2.0
Document Version: 1.0.0