08.07 Building a Script Against the API
End-to-end example scripts (bash and Python) showing how to authenticate, list risks, create a risk, update a risk, and handle errors. Use these as starting points for real integrations; productionize by adding secrets management, retries, logging, and tests.
Why this matters
Most "how do I integrate with SimpleRisk" conversations end up at the same place: a script that authenticates, hits a few endpoints, processes the responses. The Swagger UI tells you what's possible; this article shows you what the working code looks like for the most common patterns. Use the examples as starting points; productionize from there.
Prerequisites
Before running these examples:
- A SimpleRisk instance you can hit — preferably non-production for testing.
- An API Extra-active install with the
apimaster toggle on. See API Overview. - An API key for an integration user with appropriate permissions. See API Key Management.
- The base URL of your instance.
- For bash examples:
curlandjqinstalled. - For Python examples: Python 3.8+ and the
requestslibrary (pip install requests).
In all examples below, replace placeholders:
→ e.g.,simplerisk.example.com.→ the 64-character API key you generated.
Example 1: List risks (bash)
The simplest integration: authenticate and read the risk list.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="https://
" API_KEY="${SIMPLERISK_API_KEY:?Set SIMPLERISK_API_KEY environment variable}" response=$(curl -sS \ -H "X-API-KEY: ${API_KEY}" \ -H "Accept: application/json" \ "${BASE_URL}/api/v2/risks") # Check the response status code status=$(echo "${response}" | jq -r '.status_code') if [ "${status}" != "200" ]; then echo "Error: status ${status}, message: $(echo "${response}" | jq -r '.status_message')" >&2 exit 1 fi # Print risk count and a summary count=$(echo "${response}" | jq '.data | length') echo "Found ${count} risks" echo "${response}" | jq -r '.data[] | "\(.id): \(.subject) (status: \(.status))"'
Save as list_risks.sh, make executable (chmod +x list_risks.sh), set SIMPLERISK_API_KEY in your environment, run.
The pattern: curl with the header, jq to parse JSON, error-check on status_code. Adapt to your needs.
Example 2: List risks (Python)
The same operation in Python:
#!/usr/bin/env python3
"""List risks from SimpleRisk."""
import os
import sys
import requests
BASE_URL = "https://
" API_KEY = os.environ.get("SIMPLERISK_API_KEY") if not API_KEY: print("Set SIMPLERISK_API_KEY environment variable", file=sys.stderr) sys.exit(1) response = requests.get( f"{BASE_URL}/api/v2/risks", headers={ "X-API-KEY": API_KEY, "Accept": "application/json", }, timeout=30, ) if response.status_code != 200: print(f"Error: status {response.status_code}, body: {response.text}", file=sys.stderr) sys.exit(1) data = response.json() risks = data.get("data", []) print(f"Found {len(risks)} risks") for risk in risks: print(f" {risk['id']}: {risk['subject']} (status: {risk.get('status')})")
Save as list_risks.py, set SIMPLERISK_API_KEY, run with python3 list_risks.py.
Example 3: Create a risk (Python)
A more involved example: submit a new risk via POST /api/v2/risks/submit.
#!/usr/bin/env python3
"""Submit a new risk to SimpleRisk."""
import os
import sys
import requests
BASE_URL = "https://
" API_KEY = os.environ.get("SIMPLERISK_API_KEY") if not API_KEY: print("Set SIMPLERISK_API_KEY environment variable", file=sys.stderr) sys.exit(1) # The new risk's data new_risk = { "subject": "Vendor X has not completed annual security questionnaire", "reference_id": "VRM-2026-042", "regulation": 1, # ID of the relevant regulation lookup "control_number": "CC9.2", # the control reference "location": [3], # location IDs (an array because location is multi-select) "source": 2, # source ID "category": 5, # category ID "team": [4, 7], # team IDs the risk should be visible to "technology": [1, 2], # technology IDs "owner": 12, # owner user ID "manager": 8, # manager user ID "assessment": "The vendor's annual security questionnaire was due 2026-03-31 and has not been received. Without it we lack confirmation of their security posture.", "notes": "Initial follow-up sent 2026-04-15; second follow-up 2026-04-30.", # Scoring (using Classic methodology — adjust per your install) "scoring_method": 1, "CLASSIC_likelihood": 3, "CLASSIC_impact": 4, } response = requests.post( f"{BASE_URL}/api/v2/risks/submit", headers={ "X-API-KEY": API_KEY, "Accept": "application/json", "Content-Type": "application/json", }, json=new_risk, timeout=30, ) if response.status_code not in (200, 201): print(f"Error: status {response.status_code}", file=sys.stderr) print(f"Response: {response.text}", file=sys.stderr) sys.exit(1) result = response.json() risk_id = result.get("data", {}).get("id") print(f"Created risk {risk_id}")
The exact field set varies by your install's configuration (custom fields, scoring methodology, dropdown values). Use the Swagger UI's POST /risks/submit schema as the canonical reference for what fields are accepted.
Example 4: Update a risk (Python)
Modify an existing risk via PATCH /api/v2/risks/{id}:
#!/usr/bin/env python3
"""Update a risk's status in SimpleRisk."""
import os
import sys
import requests
BASE_URL = "https://
" API_KEY = os.environ.get("SIMPLERISK_API_KEY") RISK_ID = sys.argv[1] if len(sys.argv) > 1 else None if not API_KEY: print("Set SIMPLERISK_API_KEY environment variable", file=sys.stderr) sys.exit(1) if not RISK_ID: print("Usage: update_risk.py
", file=sys.stderr) sys.exit(1) # Fields to update update = { "status": "Closed", "close_reason": 1, "notes": "Closed via integration; vendor questionnaire received 2026-05-12.", } response = requests.patch( f"{BASE_URL}/api/v2/risks/{RISK_ID}", headers={ "X-API-KEY": API_KEY, "Accept": "application/json", "Content-Type": "application/json", }, json=update, timeout=30, ) if response.status_code != 200: print(f"Error: status {response.status_code}", file=sys.stderr) print(f"Response: {response.text}", file=sys.stderr) sys.exit(1) print(f"Updated risk {RISK_ID}")
The PATCH endpoint accepts only the fields you want to change; omitted fields are unchanged.
Example 5: Polling for changes
For "near-real-time" integration without webhooks: poll the risk list periodically, dispatch on changes.
#!/usr/bin/env python3
"""Poll SimpleRisk for risk changes and dispatch to a downstream system."""
import json
import os
import sys
import time
from pathlib import Path
import requests
BASE_URL = "https://
" API_KEY = os.environ.get("SIMPLERISK_API_KEY") STATE_FILE = Path("./risk_state.json") POLL_INTERVAL = 300 # 5 minutes def fetch_risks(): response = requests.get( f"{BASE_URL}/api/v2/risks", headers={"X-API-KEY": API_KEY, "Accept": "application/json"}, timeout=30, ) response.raise_for_status() return {r["id"]: r for r in response.json().get("data", [])} def load_state(): if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text()) return {} def save_state(state): STATE_FILE.write_text(json.dumps(state)) def dispatch_change(event_type, risk): """Replace this with your downstream integration (Slack post, etc.)""" print(f"[{event_type}] {risk['id']}: {risk['subject']}") def main(): while True: try: current = fetch_risks() previous = load_state() # New risks for risk_id, risk in current.items(): if str(risk_id) not in previous: dispatch_change("NEW", risk) # Removed risks for risk_id in previous: if risk_id not in {str(k) for k in current}: dispatch_change("REMOVED", {"id": risk_id, "subject": "(removed)"}) # Update state save_state({str(k): v for k, v in current.items()}) except requests.RequestException as e: print(f"Polling failed: {e}", file=sys.stderr) # Don't update state on failure; retry next cycle time.sleep(POLL_INTERVAL) if __name__ == "__main__": main()
This is a starting-point pattern. Productionize:
- Detect changes (not just new/removed) by comparing fields.
- Use exponential backoff on persistent failures.
- Log to a structured logger, not stdout.
- Run as a long-running service (systemd, Docker, k8s) rather than a script.
- Add metrics (polls per minute, dispatch count, error rate).
Productionizing scripts
Scripts that work in dev need work to be production-ready:
Secrets management
Don't export SIMPLERISK_API_KEY=... in a developer's shell history. Use:
- A secrets manager (Vault, AWS Secrets Manager, Azure Key Vault) — fetch the key at runtime.
- Encrypted environment files (sops, encrypted .env) loaded by the deployment environment.
- Per-developer keys with short rotation cycles for development; per-environment keys for staging/production.
Error handling and retries
Real integrations encounter:
- Transient network errors (retry with backoff).
- 5xx errors from SimpleRisk during deploys (retry with backoff).
- 401/403 errors (don't retry; alert).
- 429 errors if a reverse proxy enforces rate limits (retry with
Retry-After). - Malformed responses (don't crash; log and skip).
Use a sturdy HTTP client library (Python's requests with urllib3.util.retry.Retry, or higher-level libs like tenacity) that handles retry/backoff without you reinventing the wheel.
Logging
Log structured events: timestamp, request URL, response status, response time, integration-specific context. Avoid logging full response bodies (risk descriptions can contain sensitive content). Send logs to a centralized aggregator (ELK, Splunk, Datadog) for searchability and alerting.
Testing
Write tests:
- Unit tests for the parsing and processing logic (mock the API responses).
- Integration tests against a non-production SimpleRisk instance (use a dedicated test user / API key).
- Smoke tests that run after deployment to confirm the integration is working end-to-end.
Monitoring and alerting
Production integrations should emit metrics: requests per minute, error rate, dispatch latency. Alert on:
- Error rate above a threshold (the integration is failing).
- Zero requests for an extended period (the integration is hung).
- Authentication failures (the key was rotated or revoked).
Deployment
Run the integration as a long-running service:
- Cron for periodic scripts.
- systemd services for long-running daemons.
- Docker / Kubernetes for containerized deployments.
- AWS Lambda / Cloud Functions for event-driven serverless patterns.
The choice depends on the integration's lifecycle and your operational stack.
Common pitfalls
A handful of patterns recur with API scripts.
-
Hardcoding the API key in the script. Use environment variables or a secrets manager.
-
Hardcoding the base URL. Same — use environment variables. Same script should work against dev, staging, and production with only configuration changes.
-
No timeout on requests. A hung SimpleRisk causes the script to hang indefinitely. Always set a timeout.
-
No error handling. Production scripts encounter errors; handle them gracefully.
-
Polling at high frequency. 1-second polling is rarely necessary; 5-minute polling is plenty for most use cases. Be a good citizen.
-
Treating the script as the integration. A bash script in a developer's home directory isn't an integration. Move it to a deployment surface (cron, systemd, Docker) with monitoring and operational ownership.
-
Not version-pinning dependencies. A Python script that uses
requests==2.30.0in dev but the latest in production might behave differently. -
Not documenting the script. Future-you (or your colleagues) will need to know what it does, why, and how to operate it.
-
Blocking the main thread on long operations. A script that does a long-running operation per risk in a loop can take hours; consider concurrency (with appropriate rate limiting) or async patterns.
-
Logging API keys, full responses with sensitive content, or other secrets. Restrain what you log.
Related
- API Overview
- Authentication and API Keys
- Permissions and the API
- Rate Limiting and Quotas
- The OpenAPI Documentation
- Using the Postman Collection
- Using the API for Integrations
- Webhook Integration
Reference
- Bash dependencies:
curl,jq. - Python dependencies: Python 3.8+,
requestslibrary. - Authentication:
X-API-KEYheader. - Standard URL pattern:
https://— always use/api/v2/ /api/v2/explicitly. - Endpoint reference: The Swagger UI at
/api/v2/documentation.phpis the canonical source for endpoint shapes, parameters, and response formats. - Error handling baseline: Check the response's
status_code(or HTTP status); handle 4xx (client errors — fix the request, don't retry) and 5xx (server errors — retry with backoff) differently.